#!/usr/bin/env bash
#===============================================================================
# Loki Mode CLI Wrapper
# Command-line interface for Loki Mode
#
# Installation:
#   ln -sf ~/.claude/skills/loki-mode/autonomy/loki /usr/local/bin/loki
#
# Usage:
#   loki start [PRD]      - Start Loki Mode (optionally with PRD)
#   loki stop             - Stop execution immediately
#   loki cleanup          - Kill orphaned processes from crashed sessions
#   loki pause            - Pause after current session
#   loki resume           - Resume paused execution
#   loki status           - Show current status
#   loki dashboard        - Open dashboard in browser
#   loki import           - Import GitHub issues
#   loki github [cmd]     - GitHub integration (sync|export|pr|status)
#   loki help             - Show this help
#===============================================================================

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# v7.4.5 (BUG-15 bash route): honor NO_COLOR convention (https://no-color.org/)
# Mirrors the equivalent fix in loki-ts/src/util/colors.ts. Required for parity
# when the shim falls through to the bash route (e.g., when bun is missing).
if [ -n "${NO_COLOR:-}" ]; then
    RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; DIM=''; NC=''
fi

# v7.4.10: opt-in debug tracer. When LOKI_DEBUG is set (any non-empty value)
# every loki_debug call emits "[loki-debug] <ISO-timestamp> <msg>" to stderr.
# When unset it is a true no-op (no syscalls, zero observable output).
loki_debug() {
    if [ -z "${LOKI_DEBUG:-}" ]; then return 0; fi
    local ts
    ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "?")
    printf '[loki-debug] %s %s\n' "$ts" "$*" >&2
}

# Logging functions (portable across bash/zsh)
log_info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_debug() { echo -e "${CYAN}[DEBUG]${NC} $*"; }

# Source TUI library if available (spinners, progress bars, tables, diffs)
_LOKI_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$_LOKI_SCRIPT_DIR/tui.sh" ]; then
    # shellcheck source=tui.sh
    source "$_LOKI_SCRIPT_DIR/tui.sh"
fi

# Portable lock helper -- provides safe_acquire_lock / safe_release_lock.
# Needed here (not just in run.sh) because ensure_dashboard_venv tears down and
# rebuilds the HOST-GLOBAL ~/.loki/dashboard-venv, so two concurrent CLI
# invocations can otherwise delete the venv the other is importing from.
# Self-guarded against double-source.
if [ -f "$_LOKI_SCRIPT_DIR/lib/lock.sh" ]; then
    # shellcheck source=lib/lock.sh
    source "$_LOKI_SCRIPT_DIR/lib/lock.sh"
fi

# Default per-run spend cap (single source: default_max_cost_usd in loki-ts/data/model-pricing.json,
# also read by loki-ts/src/e10ext/budget_cap.ts and autonomy/run.sh). Prints a plain number; 100 only if the file is unreadable.
loki_budget_default_usd() {
    local f="${1:-$_LOKI_SCRIPT_DIR/../loki-ts/data/model-pricing.json}" v=""
    if [ -e "$f" ]; then
        v="$(sed -n 's/.*"default_max_cost_usd"[[:space:]]*:[[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$f" 2>/dev/null | head -1)"
        case "$v" in
            '' | *[!0-9.]*)
                printf '%s\n' "loki: warning: cannot read default_max_cost_usd from $f (unreadable or no parsable key); using fallback 100" >&2
                v=""
                ;;
        esac
    fi
    printf '%s\n' "${v:-100}"
}

# Guarded browser opener (loki_open_url). Every browser open goes through it
# so tests, CI and non-TTY runs never pop a tab. If missing, nothing opens.
if [ -f "$_LOKI_SCRIPT_DIR/lib/browser-open.sh" ]; then
    # shellcheck source=lib/browser-open.sh
    source "$_LOKI_SCRIPT_DIR/lib/browser-open.sh"
fi

# Crash-reporting helpers (provides loki_collection_enabled, used by
# cmd_telemetry status and cmd_crash). Self-guarded against double-source.
if [ -f "$_LOKI_SCRIPT_DIR/crash.sh" ]; then
    # shellcheck source=crash.sh
    source "$_LOKI_SCRIPT_DIR/crash.sh"
fi

# Provider install offer (v7.29.0): shared, self-contained helper providing
# detect_any_provider, offer_provider_install, and provider_offer_gate. Sourced
# here for the bash route; doctor.ts invokes the same file via child_process so
# the offer copy never drifts between routes. Self-guarded against double-source.
if [ -f "$_LOKI_SCRIPT_DIR/provider-offer.sh" ]; then
    # shellcheck source=provider-offer.sh
    source "$_LOKI_SCRIPT_DIR/provider-offer.sh"
fi

# caveman ACTIVATION level for FREE-FORM (narration) claude subcalls in this CLI
# (migration phase exec, migration docs-gen, heal execution). Prints the level to
# pass as CAVEMAN_DEFAULT_MODE when activation is warranted, else nothing. autonomy/
# loki does NOT source claude-flags.sh globally, so this lazily sources it on demand
# (same pattern as the ultrareview/workflows gates and autonomy/lib/voter-agents.sh).
# The gate (provider==claude, claude on PATH, opt-in, no legacy-completion-match,
# no-raise of a user's lower global level) lives entirely in loki_caveman_activate_env;
# this is just the lazy-source wrapper. Degrades to empty (bare invocation, byte-
# identical to pre-caveman) when the lib is missing or the helper is unavailable.
# NEVER use this for captured-output / parsed subcalls -- those HARD-SUPPRESS with
# CAVEMAN_DEFAULT_MODE=off (see _docs_invoke_provider and run.sh:3286).
_loki_caveman_narration_level() {
    if ! declare -F loki_caveman_activate_env >/dev/null 2>&1; then
        local _cm_lib=""
        if [ -f "${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" ]; then
            _cm_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh"
        elif [ -f "$(dirname "$0")/lib/claude-flags.sh" ]; then
            _cm_lib="$(dirname "$0")/lib/claude-flags.sh"
        fi
        if [ -n "$_cm_lib" ]; then
            # shellcheck source=autonomy/lib/claude-flags.sh
            . "$_cm_lib" 2>/dev/null || true
        fi
    fi
    if declare -F loki_caveman_activate_env >/dev/null 2>&1; then
        loki_caveman_activate_env
    fi
}

# P12-26b: permission flags for the migrate/doc/heal claude launches. Lazily sources
# claude-flags.sh (like the caveman wrapper above) and fills _LOKI_PERM_ARGV; unset
# LOKI_PERMISSION_POLICY=legacy keeps --dangerously-skip-permissions byte-identically.
_loki_perm_argv_for() {
    # Also sets _LOKI_CLAUDE_SHELL, the SHELL value every claude spawn in this CLI
    # pins (/bin/bash; LOKI_E10_KEEP_USER_SHELL=1 keeps the user's). Computed in a
    # subshell so the helper's top-level exports never leak. Falls back to the
    # user's SHELL when the lib is unavailable. Spawn sites that need only the
    # shell call this with output discarded.
    _LOKI_CLAUDE_SHELL="$(
        # shellcheck source=autonomy/lib/claude-flags.sh
        . "${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" >/dev/null 2>&1 || exit 1
        _loki_claude_shell_value
    )" || _LOKI_CLAUDE_SHELL=""
    [ -n "$_LOKI_CLAUDE_SHELL" ] || _LOKI_CLAUDE_SHELL="${SHELL-}"
    _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
    # Legacy policy: return before sourcing anything (claude-flags.sh exports caveman vars).
    [ "${LOKI_PERMISSION_POLICY:-}" = "legacy" ] && return 0
    _LOKI_PERM_ARGV=(--permission-mode plan)
    local _pa_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" _pa_out _pa_a
    if [ ! -f "$_pa_lib" ]; then
        echo "[loki] WARNING: compiled permission policy: helper missing; using most restrictive mode (plan)" >&2
        return 0
    fi
    # Subshell so the helper's top-level exports never leak into this shell.
    _pa_out="$(
        LOKI_PROJECT_DIR="${1:-${LOKI_PROJECT_DIR:-$PWD}}"
        # shellcheck source=autonomy/lib/claude-flags.sh
        . "$_pa_lib" >/dev/null 2>&1 || exit 1
        declare -F _loki_claude_permission_argv >/dev/null 2>&1 || exit 1
        _loki_claude_permission_argv
        printf '%s\n' "${_LOKI_PERM_ARGV[@]}"
    )" || return 0
    [ -n "$_pa_out" ] || return 0
    _LOKI_PERM_ARGV=()
    while IFS= read -r _pa_a; do _LOKI_PERM_ARGV+=("$_pa_a"); done <<< "$_pa_out"
    return 0
}

# loki edit: one-pass small change fast path (functions-only, self-guarded).
if [ -f "$_LOKI_SCRIPT_DIR/edit.sh" ]; then
    # shellcheck source=edit.sh
    source "$_LOKI_SCRIPT_DIR/edit.sh"
fi

# Quickstart guided interview (v7.29.0): provides cmd_quickstart and its
# deterministic offline template matcher. Functions-only file (no top-level
# command), sourced here so the dispatch case can call cmd_quickstart. Composes
# the provider offer (above), show_prd_plan, and cmd_start. Self-guarded.
if [ -f "$_LOKI_SCRIPT_DIR/quickstart.sh" ]; then
    # shellcheck source=quickstart.sh
    source "$_LOKI_SCRIPT_DIR/quickstart.sh"
fi

# Shared PRINT-ONLY pull-request advisory (provides print_pr_advice and its
# origin/compare-URL helpers). Single source of truth sourced by BOTH this CLI
# (cmd_deploy CI/CD path) and autonomy/run.sh (create_session_pr) so the two
# surfaces print byte-identical, correct git push + PR commands and cannot drift.
# Self-guarded against double-source. Existence-guarded matching the pattern above.
if [ -f "$_LOKI_SCRIPT_DIR/lib/git-pr-advisory.sh" ]; then
    # shellcheck source=autonomy/lib/git-pr-advisory.sh
    source "$_LOKI_SCRIPT_DIR/lib/git-pr-advisory.sh"
fi

# Proven PR (Loop 6): shared print-only Evidence Receipt renderer for PR bodies
# (render_evidence_receipt_md). Sourced so cmd_github can append the receipt to
# the PR body. Self-guarded against double-source; existence-guarded.
if [ -f "$_LOKI_SCRIPT_DIR/lib/proof-pr.sh" ]; then
    # shellcheck source=autonomy/lib/proof-pr.sh
    source "$_LOKI_SCRIPT_DIR/lib/proof-pr.sh"
fi

# Unified config-file support (#691). Canonical LOKI_CONFIG_MAP + the config-file
# loader (loki_maybe_apply_config_file / loki_apply_config_file) and the
# example/schema/validate generators. Side-effect-free on source: defines the
# array + functions, exports nothing. The main() pre-pass and run.sh's YAML
# parsers both call into this single lib so the mapping cannot drift.
if [ -f "$_LOKI_SCRIPT_DIR/lib/config-map.sh" ]; then
    # shellcheck source=autonomy/lib/config-map.sh
    source "$_LOKI_SCRIPT_DIR/lib/config-map.sh"
fi

# v8.1: one-switch SDK activation resolver (LOKI_SDK_MODE=off|judges|full,
# default off). Idempotent; run.sh sources+resolves it too. Sourced here so any
# in-CLI SDK judge site sees the resolved per-site flags. No-op when mode unset.
if [ -f "$_LOKI_SCRIPT_DIR/lib/sdk-mode.sh" ]; then
    # shellcheck source=autonomy/lib/sdk-mode.sh
    source "$_LOKI_SCRIPT_DIR/lib/sdk-mode.sh"
    loki_sdk_resolve_mode
fi

# Resolve the script's real path (handles symlinks)
resolve_script_path() {
    local script="$1"
    # Use realpath if available, otherwise fall back to readlink
    if command -v realpath &> /dev/null; then
        realpath "$script" 2>/dev/null || echo "$script"
    elif command -v readlink &> /dev/null; then
        # macOS readlink doesn't have -f, use a loop
        while [ -L "$script" ]; do
            local dir=$(dirname "$script")
            script=$(readlink "$script")
            [[ "$script" != /* ]] && script="$dir/$script"
        done
        echo "$script"
    else
        echo "$script"
    fi
}

# Find the skill installation
find_skill_dir() {
    local script_path
    script_path=$(resolve_script_path "$0")
    local script_dir
    script_dir=$(dirname "$script_path")

    # Check script's own parent first (most likely current), then installed skill, then cwd
    local script_parent
    script_parent="$(cd "$script_dir/.." 2>/dev/null && pwd)"
    local dirs=(
        "$script_parent"
        "$HOME/.claude/skills/loki-mode"
        "."
    )

    for dir in "${dirs[@]}"; do
        if [ -f "$dir/SKILL.md" ] && [ -f "$dir/autonomy/run.sh" ]; then
            echo "$dir"
            return 0
        fi
    done

    echo ""
    return 1
}

# Use || true to prevent set -e from exiting before error message.
# Honor a pre-set SKILL_DIR (test harnesses point it at a fixture); fall back to
# auto-detection when unset. No production code exports SKILL_DIR into a child
# loki invocation (children receive LOKI_SKILL_DIR instead), so this only changes
# behavior when a caller deliberately exports SKILL_DIR.
SKILL_DIR="${SKILL_DIR:-$(find_skill_dir)}" || true
if [ -z "$SKILL_DIR" ]; then
    # Check if installation exists but is incomplete (missing run.sh)
    _check_dir1="$HOME/.claude/skills/loki-mode"
    _check_dir2="$(dirname "$(resolve_script_path "$0")")/.."
    _incomplete_install=""

    for _dir in "$_check_dir1" "$_check_dir2"; do
        if [ -f "$_dir/SKILL.md" ] && [ ! -f "$_dir/autonomy/run.sh" ]; then
            _incomplete_install="$_dir"
            break
        fi
    done

    if [ -n "$_incomplete_install" ]; then
        echo -e "${RED}Error: Loki Mode installation is incomplete${NC}"
        echo "Missing: $_incomplete_install/autonomy/run.sh"
        echo ""
        echo -e "${YELLOW}Fix with:${NC}"
        echo "  npm uninstall -g loki-mode && npm install -g loki-mode"
        echo ""
        echo "This can happen when npm cache is corrupted."
    else
        echo -e "${RED}Error: Could not find Loki Mode installation${NC}"
        echo "Expected at: ~/.claude/skills/loki-mode"
        echo "Or install via: npm install -g loki-mode"
    fi
    exit 1
fi

RUN_SH="$SKILL_DIR/autonomy/run.sh"
SANDBOX_SH="$SKILL_DIR/autonomy/sandbox.sh"
EMIT_SH="$SKILL_DIR/events/emit.sh"
LEARNING_EMIT_SH="$SKILL_DIR/learning/emit.sh"
LOKI_DIR="${LOKI_DIR:-.loki}"
export LOKI_DIR

# v7.7.34: launch the autonomous runner as a SESSION/PROCESS-GROUP LEADER so the
# whole tree (orchestrator + the claude/codex/aider agent pipeline + monitors)
# shares one process group and can be killed atomically with `kill -- -PGID`.
# Without this, stopping the orchestrator pid orphans the agent child (it
# reparents to init and keeps editing files), which is the "dashboard says
# stopped but it keeps running" bug. macOS has no `setsid` binary, so prefer the
# real binary (Linux/Docker), then perl POSIX::setsid, then python3 os.setsid,
# then degrade to a plain exec (the cwd+sentinel agent sweep is the backstop).
# This execs (replaces the current process); callers use it exactly like `exec`.
#
# IMPORTANT: setsid detaches the new session from the controlling terminal, so a
# foreground INTERACTIVE `loki start` would then ignore Ctrl+C (breaking the
# documented pause/exit-on-Ctrl+C UX). In an interactive shell, job control
# ALREADY places the runner in its own process group, so the agent child shares
# it and group-kill on stop works WITHOUT setsid. We therefore only create a new
# session when stdin is NOT a tty (scripts / CI / detached launches), where the
# runner would otherwise inherit the caller's group and escape group-kill.
# Interactive runs keep Ctrl+C. Opt out entirely with LOKI_NO_NEW_SESSION=1;
# force it on (e.g. for testing) with LOKI_FORCE_NEW_SESSION=1.
_loki_new_session_exec() {
    if [ "${LOKI_NO_NEW_SESSION:-}" = "1" ]; then
        exec "$@"
    elif [ -t 0 ] && [ "${LOKI_FORCE_NEW_SESSION:-}" != "1" ]; then
        # Interactive terminal: keep the controlling tty so Ctrl+C still works.
        # We do NOT setsid here, so the runner may SHARE the interactive shell's
        # process group. Therefore we do NOT export LOKI_OWN_SESSION, and the
        # runner will NOT record loki.pgid -- group-kill is unsafe on a shared
        # shell group (it could kill the user's shell). Stop falls back to the
        # cwd + [LOKI-AUTONOMY-AGENT] sentinel sweep, which is safe and reaps the
        # agent by identity rather than by group.
        exec "$@"
    elif command -v setsid >/dev/null 2>&1; then
        LOKI_OWN_SESSION=1 exec setsid "$@"
    elif command -v perl >/dev/null 2>&1; then
        LOKI_OWN_SESSION=1 exec perl -e 'use POSIX qw(setsid); setsid(); exec @ARGV or exit 127;' "$@"
    elif command -v python3 >/dev/null 2>&1; then
        LOKI_OWN_SESSION=1 exec python3 -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' "$@"
    else
        exec "$@"
    fi
}

# Actionable PRD-not-found exit. Both PRD-path existence guards in cmd_start route
# through here so they cannot drift: the user always learns the two recoveries
# (fix the path, or drop the arg to auto-generate from the codebase). If a sibling
# .md file looks like a near-miss of the bad path, surface it honestly as a hint.
# Prints to stderr and exits 1. Honest: only suggests a "did you mean" when a real
# .md sibling exists; never fabricates a path.
_prd_not_found_die() {
    local bad_path="$1"
    echo -e "${RED}Error: PRD file not found: $bad_path${NC}" >&2
    echo -e "${YELLOW}Hint: provide a path to an existing .md/.json/.txt/.yaml/.yml file, or run 'loki start' with no args to analyze the current directory.${NC}" >&2
    # Best-effort near-miss suggestion: if a .md file sits in the same directory
    # as the bad path, point at it. Read-only; silent when nothing matches.
    local dir base suggestion
    dir="$(dirname "$bad_path" 2>/dev/null)" || dir="."
    base="$(basename "$bad_path" 2>/dev/null)"
    if [ -d "$dir" ] && [ -n "$base" ]; then
        suggestion="$(ls -1 "$dir"/*.md 2>/dev/null | head -1)"
        if [ -n "$suggestion" ]; then
            echo -e "${YELLOW}Did you mean: $suggestion ?${NC}" >&2
        fi
    fi
    exit 1
}

# Anonymous usage telemetry
PROJECT_DIR="$SKILL_DIR"
_TELEMETRY_SCRIPT="$SKILL_DIR/autonomy/telemetry.sh"
if [ -f "$_TELEMETRY_SCRIPT" ]; then
    source "$_TELEMETRY_SCRIPT"
fi

# Get version from VERSION file
get_version() {
    if [ -f "$SKILL_DIR/VERSION" ]; then
        cat "$SKILL_DIR/VERSION"
    else
        echo "unknown"
    fi
}

# Stale-install nudge (bash route; mirrors loki-ts/src/util/update_check.ts).
# Best-effort, fail-silent, and adds zero latency on the hot path:
#   - skipped entirely on non-TTY / CI / LOKI_NO_UPDATE_CHECK=1;
#   - result cached in ~/.loki/cache/update-check-bash.json for >= 24h, so the
#     network is hit at most once a day (separate file from the Bun cache, which
#     stores millisecond timestamps; this file stores Unix seconds);
#   - the registry fetch has a hard 1.5s timeout and is fully fail-silent;
#   - the hint goes to STDERR so stdout stays byte-identical for scripts and the
#     bash<->Bun parity tests.
# We never fabricate a version: the hint prints only when a real check (or a
# fresh cache from one) reports a strictly newer stable semver.
_loki_update_cache_path() {
    echo "$HOME/.loki/cache/update-check-bash.json"
}

# _loki_verdict_badge <headline> <url> -> prints a "Verified by Loki" markdown
# badge for an honesty.headline, or nothing. Exact-match only: VERIFIED
# (brightgreen), VERIFIED WITH GAPS (orange), NOT VERIFIED (red). Any other
# headline, or an empty url, omits the badge (never green, never forced red).
_loki_verdict_badge() {
    local headline="${1:-}" url="${2:-}" message color
    url="${url#"${url%%[![:space:]]*}"}"; url="${url%"${url##*[![:space:]]}"}"
    [ -n "$url" ] || return 0
    case "$headline" in
        "VERIFIED") message="verified"; color="brightgreen" ;;
        "VERIFIED WITH GAPS") message="verified with gaps"; color="orange" ;;
        "NOT VERIFIED") message="not verified"; color="red" ;;
        *) return 0 ;;
    esac
    printf '[![Loki: %s](https://img.shields.io/static/v1?label=Loki&message=%s&color=%s)](%s)\n' \
        "$message" "${message// /%20}" "$color" "$url"
}

# _loki_semver_newer <latest> <current> -> 0 (true) iff latest is strictly newer.
# Rejects anything that is not a plain MAJOR.MINOR.PATCH (e.g. "unknown",
# prereleases): on any unparseable input it returns 1 (never nudge).
_loki_semver_newer() {
    local latest="$1" current="$2"
    case "$latest" in
        [0-9]*.[0-9]*.[0-9]*) ;;
        *) return 1 ;;
    esac
    case "$current" in
        [0-9]*.[0-9]*.[0-9]*) ;;
        *) return 1 ;;
    esac
    # Reject prerelease / build suffixes (anything past the third number).
    case "$latest" in *[!0-9.]*) return 1 ;; esac
    case "$current" in *[!0-9.]*) return 1 ;; esac
    local l_maj l_min l_pat c_maj c_min c_pat
    IFS='.' read -r l_maj l_min l_pat _ <<EOF
$latest
EOF
    IFS='.' read -r c_maj c_min c_pat _ <<EOF
$current
EOF
    # Default any empty component to 0 (defensive).
    l_maj=${l_maj:-0}; l_min=${l_min:-0}; l_pat=${l_pat:-0}
    c_maj=${c_maj:-0}; c_min=${c_min:-0}; c_pat=${c_pat:-0}
    if [ "$l_maj" -ne "$c_maj" ]; then [ "$l_maj" -gt "$c_maj" ]; return; fi
    if [ "$l_min" -ne "$c_min" ]; then [ "$l_min" -gt "$c_min" ]; return; fi
    [ "$l_pat" -gt "$c_pat" ]
}

# _loki_latest_published_version: print the latest published version (fresh
# cache first, else one short registry fetch that refreshes the cache), or
# nothing when offline. Optional $1 = curl timeout seconds (default 3).
# Never fabricates, always returns 0.
_loki_latest_published_version() {
    local timeout="${1:-3}" cache_file latest="" checked_at now age
    cache_file=$(_loki_update_cache_path)
    if [ -f "$cache_file" ]; then
        checked_at=$(sed -n 's/.*"checkedAt"[[:space:]]*:[[:space:]]*\([0-9]\{1,\}\).*/\1/p' "$cache_file" 2>/dev/null | head -1)
        latest=$(sed -n 's/.*"latest"[[:space:]]*:[[:space:]]*"\([0-9][0-9.]*\)".*/\1/p' "$cache_file" 2>/dev/null | head -1)
        now=$(date +%s 2>/dev/null || echo 0)
        if [ -n "$checked_at" ] && [ -n "$latest" ]; then
            age=$(( now - checked_at ))
            if [ "$age" -ge 0 ] && [ "$age" -lt 86400 ]; then
                printf '%s\n' "$latest"
                return 0
            fi
        fi
        latest=""
    fi
    if command -v curl >/dev/null 2>&1; then
        latest=$(curl -fsS --max-time "$timeout" "https://registry.npmjs.org/loki-mode/latest" 2>/dev/null \
            | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([0-9][0-9.]*\)".*/\1/p' | head -1)
    fi
    case "$latest" in
        [0-9]*.[0-9]*.[0-9]*) ;;
        *) return 0 ;;
    esac
    ( mkdir -p "$(dirname "$cache_file")" 2>/dev/null \
        && printf '{"checkedAt":%s,"latest":"%s"}\n' "$(date +%s 2>/dev/null || echo 0)" "$latest" > "$cache_file" 2>/dev/null ) || true
    printf '%s\n' "$latest"
    return 0
}

# maybe_print_update_hint: print the one-line update nudge to stderr if (and
# only if) a successful, non-fabricated check reports a strictly newer release.
# Never blocks, never slows the command, never touches stdout. Always returns 0.
maybe_print_update_hint() {
    # Opt-out / non-interactive / CI: do nothing, touch neither disk nor network.
    [ "${LOKI_NO_UPDATE_CHECK:-}" = "1" ] && return 0
    # Air-gap: no outbound attempt of any kind, independent of the opt-out above.
    [ "${LOKI_AIRGAP:-}" = "true" ] && return 0
    [ -n "${CI:-}" ] && return 0
    [ ! -t 1 ] && return 0

    local current
    current=$(get_version)
    case "$current" in
        [0-9]*.[0-9]*.[0-9]*) ;;
        *) return 0 ;;  # "unknown" or non-semver -> never nudge
    esac

    # Fresh cache means zero network; otherwise one short fail-silent fetch.
    # No usable result -> print nothing (never fabricate).
    local latest
    latest=$(_loki_latest_published_version 1.5)
    [ -n "$latest" ] || return 0
    if _loki_semver_newer "$latest" "$current"; then
        printf 'A newer loki-mode is available (%s). Update: bun install -g loki-mode\n' "$latest" >&2
    fi
    return 0
}

# Ensure dashboard Python venv with all deps installed.
# Uses ~/.loki/dashboard-venv (persistent, writable, survives npm/brew upgrades).
# Sets DASHBOARD_PYTHON to the venv python3 path on success.
# Returns 0 on success, 1 on failure.
ensure_dashboard_venv() {
    local dashboard_venv="$HOME/.loki/dashboard-venv"
    DASHBOARD_PYTHON="python3"

    # Use existing venv python if available
    if [ -x "${dashboard_venv}/bin/python3" ]; then
        DASHBOARD_PYTHON="${dashboard_venv}/bin/python3"
    fi

    # Check all required imports
    if "$DASHBOARD_PYTHON" -c "import fastapi; import sqlalchemy; import aiosqlite" 2>/dev/null; then
        return 0
    fi

    # The venv is HOST-GLOBAL, so concurrent runs must not rebuild it at once:
    # one run's `rm -rf` would delete the tree another is importing from. Lock
    # the venv path itself -- safe_acquire_lock appends ".lockdir", giving a
    # SIBLING mutex that the teardown below cannot destroy. Timeout must outlast
    # a real cold `python3 -m venv` + `pip install` (not the 5s used by the
    # JSON read-modify-write call sites).
    local _venv_locked=false
    if type safe_acquire_lock >/dev/null 2>&1; then
        if safe_acquire_lock "$dashboard_venv" "${LOKI_VENV_LOCK_TIMEOUT:-300}"; then
            _venv_locked=true
            # The winner may have finished the build while we waited -- re-probe
            # before tearing anything down.
            DASHBOARD_PYTHON="python3"
            [ -x "${dashboard_venv}/bin/python3" ] && DASHBOARD_PYTHON="${dashboard_venv}/bin/python3"
            if "$DASHBOARD_PYTHON" -c "import fastapi; import sqlalchemy; import aiosqlite" 2>/dev/null; then
                safe_release_lock "$dashboard_venv"
                return 0
            fi
        else
            # Do NOT fall through into the rm -rf unlocked -- that is the exact
            # race this lock exists to prevent.
            echo -e "${RED}Timed out waiting for another run to finish building the dashboard venv${NC}"
            echo "  Retry, or remove a stale lock: rm -rf ${dashboard_venv}.lockdir"
            return 1
        fi
    fi

    echo -e "${YELLOW}Setting up dashboard virtualenv...${NC}"

    # Create venv if missing or broken
    if ! [ -x "${dashboard_venv}/bin/python3" ]; then
        # Remove broken venv if exists
        if [ -d "$dashboard_venv" ]; then
            echo -e "${YELLOW}Removing broken dashboard venv...${NC}"
            rm -rf "$dashboard_venv"
        fi
        mkdir -p "$HOME/.loki"
        python3 -m venv "$dashboard_venv" 2>/dev/null || python3.13 -m venv "$dashboard_venv" 2>/dev/null || {
            echo -e "${RED}Failed to create dashboard virtualenv${NC}"
            echo "You may need to install python3-venv:"
            echo "  sudo apt install python3-venv    (Debian/Ubuntu)"
            echo "  brew install python3              (macOS)"
            [ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
            return 1
        }
    fi

    DASHBOARD_PYTHON="${dashboard_venv}/bin/python3"
    echo -e "${YELLOW}Installing dashboard dependencies...${NC}"

    # Try pinned requirements first, then unpinned fallback
    local req_file="${SKILL_DIR}/dashboard/requirements.txt"
    local installed=false
    if [ -f "$req_file" ]; then
        if "${dashboard_venv}/bin/pip" install -r "$req_file" 2>&1 | tail -1; then
            installed=true
        else
            echo -e "${YELLOW}Pinned deps failed, trying unpinned...${NC}"
        fi
    fi

    if [ "$installed" = false ]; then
        # Install without greenlet first (it may need a C compiler).
        # SQLAlchemy only requires greenlet for sync-in-async; aiosqlite is pure async.
        if ! "${dashboard_venv}/bin/pip" install fastapi uvicorn pydantic websockets sqlalchemy aiosqlite httpx pexpect watchdog 2>&1 | tail -1; then
            echo -e "${RED}Failed to install dashboard dependencies${NC}"
            echo "Try manually: ${dashboard_venv}/bin/pip install fastapi uvicorn sqlalchemy aiosqlite"
            [ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
            return 1
        fi
        # Try greenlet separately (optional, needs C compiler on some platforms)
        "${dashboard_venv}/bin/pip" install greenlet 2>/dev/null || true
    fi

    # Verify imports work after install
    if ! "$DASHBOARD_PYTHON" -c "import fastapi; import sqlalchemy; import aiosqlite" 2>/dev/null; then
        echo -e "${RED}Dashboard dependencies installed but imports still fail${NC}"
        echo "Try removing the venv and retrying:"
        echo "  rm -rf ${dashboard_venv}"
        echo "  loki dashboard start"
        [ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
        return 1
    fi

    [ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
    return 0
}

# Check if jq is available (called by functions that need it)
require_jq() {
    if ! command -v jq &> /dev/null; then
        echo -e "${RED}Error: jq is required but not installed.${NC}"
        echo "Install with:"
        echo "  brew install jq    (macOS)"
        echo "  apt install jq     (Debian/Ubuntu)"
        echo "  yum install jq     (RHEL/CentOS)"
        return 1
    fi
}

# --- remote submit (loki start --remote <url>) --------------------------------
#
# Submits a spec to a deployed trigger-server's POST /jobs instead of building
# locally, then polls GET /jobs/<id> until the job reaches a terminal state.
#
# The bearer token is read from LOKI_REMOTE_TOKEN or LOKI_REMOTE_TOKEN_FILE and
# is NEVER placed in argv (the process list is world-readable) and never logged.
# It reaches curl through `--config -` on stdin.

# Extract the host from a URL: strip scheme, userinfo, then cut at the first
# '/', ':' or '?'. Compared EXACTLY, never by prefix -- "http://localhost*"
# would happily accept http://localhost.evil.com and leak a bearer token.
loki_remote_host() {
    local url="$1"
    local rest="${url#*://}"
    rest="${rest##*@}"
    rest="${rest%%/*}"
    rest="${rest%%\?*}"
    # Strip a :port, but keep an [::1] literal intact.
    case "$rest" in
        \[*\]*) rest="${rest%%\]*}]" ;;
        *) rest="${rest%%:*}" ;;
    esac
    printf '%s' "$rest"
}

loki_remote_is_local_host() {
    case "$(loki_remote_host "$1")" in
        localhost|127.0.0.1|\[::1\]|::1) return 0 ;;
        *) return 1 ;;
    esac
}

# Read the token from env or a file. Never echoed, never passed as an argument.
loki_remote_token() {
    if [ -n "${LOKI_REMOTE_TOKEN:-}" ]; then
        printf '%s' "$LOKI_REMOTE_TOKEN"
        return 0
    fi
    if [ -n "${LOKI_REMOTE_TOKEN_FILE:-}" ] && [ -f "${LOKI_REMOTE_TOKEN_FILE}" ]; then
        # Mounted secrets end with a newline; strip it or every compare fails.
        tr -d '\r\n' < "${LOKI_REMOTE_TOKEN_FILE}"
        return 0
    fi
    return 1
}

# curl with the Authorization header fed on stdin via --config, so the token
# never appears in argv. Usage: loki_remote_curl <token> <curl-args...>
loki_remote_curl() {
    local _tok="$1"; shift
    printf 'header = "Authorization: Bearer %s"\n' "$_tok" \
        | curl --config - "$@"
}

loki_remote_submit() {
    local url="$1"
    local spec="$2"

    url="${url%/}"

    if [ -z "$spec" ]; then
        echo -e "${RED}loki: --remote needs a spec to submit${NC}" >&2
        echo "  e.g. loki start ./prd.md --remote $url" >&2
        return 1
    fi

    # A bearer token over plaintext to a remote host is a credential leak.
    # Loopback is exempt (it never leaves the machine); anything else needs an
    # explicit opt-in so the leak is a decision, not an accident.
    case "$url" in
        http://*)
            if ! loki_remote_is_local_host "$url" && [ "${LOKI_REMOTE_INSECURE:-}" != "1" ]; then
                echo -e "${RED}loki: refusing to send a bearer token over plaintext http to $(loki_remote_host "$url")${NC}" >&2
                echo "  Use https://, or set LOKI_REMOTE_INSECURE=1 to override." >&2
                return 1
            fi
            ;;
    esac

    local token
    if ! token="$(loki_remote_token)" || [ -z "$token" ]; then
        echo -e "${RED}loki: no remote token configured${NC}" >&2
        echo "  Set LOKI_REMOTE_TOKEN or LOKI_REMOTE_TOKEN_FILE (never pass it as an argument)." >&2
        return 1
    fi

    require_jq || return 1

    local body resp
    body="$(jq -nc --arg s "$spec" '{spec:$s}')"

    echo "Submitting to $url ..."
    if ! resp="$(loki_remote_curl "$token" -fsS --max-time 30 \
            -H "Content-Type: application/json" \
            -X POST --data "$body" "$url/jobs" 2>&1)"; then
        # NEVER fall back to a local run: a user who asked for remote and
        # silently got a local build would pay local spend they did not ask for.
        echo -e "${RED}loki: remote submit failed: $url/jobs${NC}" >&2
        echo "  Is the server reachable, and is LOKI_REMOTE_TOKEN correct?" >&2
        echo "  Not falling back to a local build (you asked for --remote)." >&2
        return 1
    fi

    local job_id
    job_id="$(printf '%s' "$resp" | jq -r '.id // empty' 2>/dev/null)"
    if [ -z "$job_id" ]; then
        echo -e "${RED}loki: remote accepted the request but returned no job id${NC}" >&2
        return 1
    fi
    echo "Job $job_id queued on $url"

    local poll_sec="${LOKI_REMOTE_POLL_SEC:-5}"
    local max_polls="${LOKI_REMOTE_MAX_POLLS:-720}"
    local status="" last="" i=0
    while [ "$i" -lt "$max_polls" ]; do
        i=$((i+1))
        if ! resp="$(loki_remote_curl "$token" -fsS --max-time 30 \
                "$url/jobs/$job_id" 2>&1)"; then
            echo -e "${RED}loki: lost contact with $url while polling job $job_id${NC}" >&2
            return 1
        fi
        status="$(printf '%s' "$resp" | jq -r '.status // empty' 2>/dev/null)"
        if [ "$status" != "$last" ]; then
            echo "  [$job_id] $status"
            last="$status"
        fi
        case "$status" in
            passed|failed|unknown|error|rejected*)
                break
                ;;
        esac
        sleep "$poll_sec"
    done

    # A TAMPERED receipt fails the whole submit regardless of the job status:
    # reporting a build as successful while holding proof that its own receipt
    # was altered would make the receipt decorative.
    local _receipt_rc=0
    loki_remote_fetch_receipt "$url" "$token" "$job_id" || _receipt_rc=$?
    if [ "$_receipt_rc" -ne 0 ]; then
        echo -e "${RED}loki: remote job $job_id returned a receipt that failed verification${NC}" >&2
        return 1
    fi

    # Gate on the BUILD's outcome, not on "a build was launched". The server
    # reports passed / failed / unknown as distinct terminal statuses (see
    # JOB_TERMINAL_STATUSES in trigger-server.py). Anything that is not
    # "passed" exits non-zero so a CI pipeline cannot go green on a failed
    # build. "unknown" -- the build detached and its exit code was never
    # observed -- fails CLOSED: an outcome nobody observed is not a pass.
    case "$status" in
        passed) return 0 ;;
        unknown)
            echo -e "${RED}loki: remote job $job_id outcome UNKNOWN${NC}" >&2
            echo "  The build detached and its exit code was never observed." >&2
            echo "  An unobserved outcome is not a pass; treating it as a failure." >&2
            return 1
            ;;
        "")     echo -e "${RED}loki: job $job_id did not reach a terminal status${NC}" >&2; return 1 ;;
        *)      echo -e "${RED}loki: remote job $job_id failed: $status${NC}" >&2; return 1 ;;
    esac
}

# Fetch the Evidence Receipt and write it where a local run puts it, so
# `loki proof` works on a remote build, then VERIFY it here -- on the machine
# that did NOT produce it. That is the entire point: a receipt you can only
# check by asking the builder whether it is good proves nothing.
#
# Returns 0 when the receipt verified or is honestly reported as unverifiable,
# and NON-ZERO when the receipt is TAMPERED. Absence of a receipt is not a
# failure (a build may legitimately produce none); a receipt that fails its
# own integrity check is.
loki_remote_fetch_receipt() {
    local url="$1" token="$2" job_id="$3"
    local dir="${LOKI_DIR:-.loki}/proofs/$job_id"
    local tmp resp

    if ! resp="$(loki_remote_curl "$token" -fsS --max-time 30 \
            "$url/jobs/$job_id/proof" 2>/dev/null)"; then
        echo "  (no evidence receipt available from $url for $job_id)" >&2
        return 0
    fi
    # Only write something that parses as JSON: a proxy error page must not
    # land in .loki/proofs/ looking like a receipt.
    if ! printf '%s' "$resp" | jq -e . >/dev/null 2>&1; then
        echo "  (remote returned a non-JSON receipt for $job_id; not written)" >&2
        return 0
    fi
    mkdir -p "$dir"
    tmp="$dir/.proof.json.$$"
    printf '%s' "$resp" > "$tmp" && mv "$tmp" "$dir/proof.json"
    echo "  Evidence Receipt: $dir/proof.json"

    # The URL is passed so the receipt's attestation can be checked against that
    # server's published JWKS. Without it the attestation is simply not checked
    # and the verdict falls back to the gpg path, which is the pre-existing
    # behaviour rather than a downgrade.
    loki_remote_verify_receipt "$dir/proof.json" "$url"
}

# Verify a fetched receipt locally and print exactly what was and was not
# proven. Exit 1 ONLY on tamper.
#
# THREE OUTCOMES, NEVER COLLAPSED INTO TWO:
#   VERIFIED  a gpg signature is present and valid  -> integrity AND provenance
#   UNSIGNED  the hash matches, no signature        -> integrity ONLY
#   TAMPERED  hash mismatch or bad signature        -> exit 1
#
# UNSIGNED is not a weaker VERIFIED, it is a different claim. docs/VERIFICATION-COST.md
# records that the unsigned path is FORGEABLE: whoever rewrites the facts and
# the headline into a mutually consistent lie and recomputes the hash passes
# every check here. A user who trusts a "verified" label meaning only "these
# bytes are self-consistent" is worse off than one who knows they hold no proof,
# because they will stop looking. So the word "verified" is never printed for an
# unsigned receipt.
#
# There is a FOURTH state and it must not be folded into UNSIGNED: a signature
# is present but gpg is not installed here, so it could not be checked. Saying
# UNSIGNED there would be a false statement about the receipt (it IS signed),
# and saying VERIFIED would be a false statement about this machine (it checked
# nothing). It gets its own line and never claims verification.
# Resolve a tool name to an absolute path, rejecting any PATH entry that is
# not itself absolute. A bare "" component in PATH (what "export PATH=x:$PATH"
# on an unset variable leaves, or an accidental "PATH=":$PATH"") is treated by
# the shell as the current directory, and so is "."; but ANY other relative
# entry (a bare "bin", "./tools", "../x") resolves against the verifier's cwd
# just as unsafely, so all of them are rejected the same way, not just those
# two literal strings. An unscoped `command -v python3`/`gpg`/`git` on a verify
# path can otherwise resolve to a committed shim sitting in the checkout being
# verified. Prints nothing (never a relative name) when no real binary is on
# a real (absolute) PATH entry, so callers fail closed the same way
# `command -v` failing does.
loki_verify_tool_path() {
    local name="$1" dir
    local IFS=:
    for dir in $PATH; do
        case "$dir" in
            /*) ;;
            *) continue ;;
        esac
        [ -x "$dir/$name" ] && [ ! -d "$dir/$name" ] && { printf '%s\n' "$dir/$name"; return 0; }
    done
    return 1
}

# Ask gpg WHY a signature failed. Echoes "nopubkey" when the signing key is
# absent (uncheckable), "" otherwise (a genuinely bad signature, or we could
# not tell). Fails toward "" so an unclear answer keeps the loud TAMPERED
# verdict rather than silently downgrading a real forgery to a soft warning.
loki_remote_gpg_status() {
    local pj="$1"
    local d="${TMPDIR:-/tmp}/loki-sigcheck.$$"
    mkdir -p "$d" || return 0
    python3 -E - "$pj" "$d" <<'PY' 2>/dev/null || { rm -rf "$d"; return 0; }
# The cwd is the checkout being verified: drop it from sys.path before any
# other import, or a committed hashlib.py/json.py could print the verdict.
# The interpreter runs with -E so PYTHONPATH (an empty component would add
# the cwd as an absolute path) and a committed sitecustomize.py cannot act
# before this line.
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, sys, os
proof = json.load(open(sys.argv[1]))
d = sys.argv[2]
sig = (proof.get("verification") or {}).get("gpg_signature") or ""
proof.pop("verification", None)
open(os.path.join(d, "canon.bin"), "wb").write(
    json.dumps(proof, sort_keys=True, separators=(",", ":")).encode())
open(os.path.join(d, "sig.asc"), "w").write(sig)
PY
    local st
    st="$(gpg --status-fd 1 --verify "$d/sig.asc" "$d/canon.bin" 2>/dev/null)"
    rm -rf "$d"
    case "$st" in
        *NO_PUBKEY*) printf 'nopubkey' ;;
        *) printf '' ;;
    esac
}

# Check a receipt's JWKS attestation against the server's PUBLISHED key set.
#
# This is the half gpg cannot do over a network. gpg proves provenance only if
# you already hold the publisher's key, which a submitter who ran
# `loki start --remote` never does -- so the honest gpg outcome there is
# NOT CHECKED, and it stays that way until someone performs an out-of-band key
# import. Fetching /.well-known/jwks.json over the same TLS the API already
# uses removes that step.
#
# Prints "ok", "bad", or "" (could not check). Fails toward "" so an
# unreachable JWKS never manufactures a verdict in either direction -- an
# absent measurement is not evidence, and inventing a PASS here would be worse
# than inventing a FAIL.
loki_remote_attestation_status() {
    local pj="$1" url="$2" _jh
    [ -n "$url" ] || { printf ''; return 0; }
    # A JWKS key set travels over the same channel as the receipt it certifies.
    # Plain http lets anyone on the network path swap in their own key set, and
    # the attestation would then "verify" against a forged key -- a MITM the
    # TLS the API already uses is meant to prevent. Only https is trusted;
    # loopback http and file:// are the exceptions, because neither one leaves
    # this machine for anyone to sit on. Checked here, before python is even
    # invoked, so a rejected URL is never fetched -- not "fetched and then
    # distrusted". Same check in loki_proof_attestation_check below -- change
    # both.
    #
    # The host is bounded exactly (up to the first "/", then stripped of any
    # port) rather than matched with a trailing glob, and a userinfo "@" is
    # rejected outright -- "http://127.0.0.1:80@evil.com" is host evil.com to
    # any real URL parser (userinfo before the @, host after), and a loose
    # `http://127.0.0.1:*` glob would wrongly wave that straight through.
    case "$url" in
        https://*|file://*) ;;
        http://*)
            _jh="${url#http://}"; _jh="${_jh%%/*}"
            case "$_jh" in
                *@*) printf ''; return 0 ;;
            esac
            # A bracketed IPv6 literal must be the WHOLE host: "[::1]" or
            # "[::1]:port", never a prefix, or "[::1].evil.com" (not itself
            # resolvable as loopback, but not rejected either) would parse as
            # loopback "::1" with everything after the first "]" silently
            # dropped instead of validated.
            case "$_jh" in
                \[*\]) _jh="${_jh#\[}"; _jh="${_jh%\]}" ;;
                \[*\]:*) _jh="${_jh#\[}"; _jh="${_jh%%\]:*}" ;;
                \[*) printf ''; return 0 ;;
                *) _jh="${_jh%%:*}" ;;
            esac
            case "$_jh" in
                127.0.0.1|localhost|::1) ;;
                *) printf ''; return 0 ;;
            esac
            ;;
        *) printf ''; return 0 ;;
    esac
    command -v python3 >/dev/null 2>&1 || { printf ''; return 0; }
    local tok
    # jq's `//` treats JSON false the same as null/missing, which is exactly
    # the semantics wanted here: `attestation: false` reads as "no attestation
    # present" (UNSIGNED/absent), not as a malformed token. Kept as a bash-level
    # short circuit for the unreachable-JWKS-avoidance case; the Python copy
    # below carries the same `is False` guard so the two never diverge if this
    # jq pre-filter is ever removed.
    tok="$(jq -r '.verification.attestation // empty' "$pj" 2>/dev/null)"
    [ -n "$tok" ] || { printf ''; return 0; }
    python3 -E - "$pj" "$url" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
# The cwd is the checkout being verified: drop it from sys.path before any
# other import, or a committed hashlib.py/json.py could print the verdict.
# The interpreter runs with -E so PYTHONPATH (an empty component would add
# the cwd as an absolute path) and a committed sitecustomize.py cannot act
# before this line.
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, sys, hashlib, urllib.request
sys.path.insert(0, sys.argv[3])
try:
    from receipt_jwt import verify_attestation, keyset_problem, check_seal_meta, _CRYPTO_AVAILABLE
except ImportError:
    sys.exit(1)          # -> "" : cannot check, never a verdict
proof = json.load(open(sys.argv[1]))
tok = (proof.get("verification") or {}).get("attestation")
# `attestation: false` is "no attestation present", the same fact as a missing
# or empty-string field -- never a malformed token. `is False` only, never
# `== False`, because `0 == False` in Python and a numeric attestation field
# is a different (still malformed) shape that must still read as TAMPERED.
if tok is None or tok == "" or tok is False:
    sys.exit(1)
try:
    with urllib.request.urlopen(
            sys.argv[2].rstrip("/") + "/.well-known/jwks.json", timeout=10) as r:
        jwks = json.loads(r.read())
except Exception:
    sys.exit(1)          # unreachable JWKS is UNCHECKED, not a failure
# A key set that is not {"keys": [{...}, ...]} -- INCLUDING an empty list -- is
# a key-set problem: UNCHECKED, never an accusation against the receipt. An
# empty keyset is an inability to check (no key was published to check
# against), not evidence of tampering. The same holds below for a set that
# gives zero or several entries, or a non-EdDSA alg, for the token's kid. Checked BEFORE anything below can print
# "bad", so a bad or empty key set can never read as TAMPERED.
_keys = jwks.get("keys") if isinstance(jwks, dict) else None
if not isinstance(_keys, list) or not _keys or not all(isinstance(k, dict) for k in _keys):
    sys.exit(1)
# The receipt's builder controls the token. An attestation that is not a JWT
# string (and not the "no attestation" `false` handled above) is a malformed
# receipt: TAMPERED, decided without any crypto.
if not isinstance(tok, str):
    print("bad"); sys.exit(0)
# Without `cryptography`, verify_attestation returns False and this would print
# "bad" -- a TAMPERED accusation for a check that never ran. A missing verifier
# dependency is UNCHECKED. Same guard in loki_proof_attestation_check below.
if not _CRYPTO_AVAILABLE:
    sys.exit(1)
# A key set that cannot give exactly one usable key for the token's kid is a
# key-set problem, not an accusation: UNCHECKED.
try:
    if keyset_problem(tok, jwks):
        sys.exit(1)
except Exception:
    print("bad"); sys.exit(0)
# Any other crash inside verification comes from the token: refuse it.
# Letting it propagate read as UNCHECKED, which let a forger turn TAMPERED
# into "install python3 cryptography".
try:
    ok, claims = verify_attestation(tok, jwks)
except Exception:
    print("bad"); sys.exit(0)
if not ok:
    print("bad"); sys.exit(0)
# The signature verifying is not enough on its own: it must attest to THIS
# receipt. Recompute the digest the same way the receipt records it (with
# `verification` stripped) and require the signed claim to match, or a valid
# token lifted from a different run would vouch for these bytes.
_vmeta = proof.pop("verification", None)
canon = json.dumps(proof, sort_keys=True, separators=(",", ":")).encode()
if claims.get("receipt_sha256") != hashlib.sha256(canon).hexdigest():
    print("bad"); sys.exit(0)
# seal.v2: the verification.* metadata (scope, algo, attestation_kid, ...) is
# inside the signature; a token that signs different metadata is refused. A
# legacy seal.v1 token still verifies (its metadata is simply unsigned).
print("ok" if check_seal_meta(tok, claims, _vmeta)[0] != "bad" else "bad")
PY
}

# Check a receipt's attestation against a key set given as a URL OR a file.
#
# The file form is what makes this usable by a third party with no network and
# no Loki install: hand them proof.json and jwks.json and they can check both
# integrity and provenance themselves. That is the whole claim -- "a receipt you
# can check yourself" is false if checking requires our infrastructure.
#
# Prints "ok", "bad", "none" (no attestation present), or "" (could not check).
# "none" and "" are deliberately distinct: one is a fact about the receipt, the
# other is an absent measurement, and collapsing them would report an unchecked
# receipt as an unattested one.
# NOTE: this shares its verification logic with loki_remote_attestation_status
# above. The two differ only in their key-set source (that one is URL-only, from
# a live fetch during a --remote run; this one also accepts a FILE for the
# offline third-party case). If you change the digest-recompute rule, CHANGE
# BOTH -- they are covered by different tests
# (test-remote-attestation-verdict.sh and test-proof-verify-jwks.sh), so a
# one-sided edit passes one suite and silently weakens the other. Mutation
# testing found exactly this: a mutation applied to the first copy left every
# assertion of the second suite green.
loki_proof_attestation_check() {
    local pj="$1" src="$2" _jh
    [ -n "$src" ] || { printf ''; return 0; }
    # Same MITM guard as loki_remote_attestation_status above -- change both.
    # Only applies to a URL source; a local file (the third-party offline case)
    # never travels a network and needs no scheme at all. The host is bounded
    # exactly and a userinfo "@" is rejected outright -- see the sibling
    # function's comment for why a trailing glob is not safe here.
    #
    # A recognized scheme prefix is checked FIRST, before any "-f" test: the
    # process cwd is the checkout being verified (attacker-controlled), and a
    # repo can ship a file whose literal name IS a URL string (for example
    # "https:/evil.example.com/.well-known/jwks.json" as a real path on disk).
    # Testing "-f" first would treat that shadow file as the local-file form
    # and hand its content straight to the caller with no network involved at
    # all -- silently defeating the "only https is trusted" guarantee this
    # function exists to provide. Scheme detection must win over the
    # filesystem every time, not just when no file happens to collide.
    case "$src" in
        https://*|http://*|file://*)
            case "$src" in
                https://*|file://*) ;;
                http://*)
                    _jh="${src#http://}"; _jh="${_jh%%/*}"
                    case "$_jh" in
                        *@*) printf ''; return 0 ;;
                    esac
                    # See loki_remote_attestation_status above for why the
                    # bracketed-IPv6 match must bound the closing bracket.
                    case "$_jh" in
                        \[*\]) _jh="${_jh#\[}"; _jh="${_jh%\]}" ;;
                        \[*\]:*) _jh="${_jh#\[}"; _jh="${_jh%%\]:*}" ;;
                        \[*) printf ''; return 0 ;;
                        *) _jh="${_jh%%:*}" ;;
                    esac
                    case "$_jh" in
                        127.0.0.1|localhost|::1) ;;
                        *) printf ''; return 0 ;;
                    esac
                    ;;
            esac
            ;;
        *)
            # A plain path is decided in python below, which reports an
            # unsigned receipt as "none" BEFORE it looks at the key file, so a
            # missing key file on an unsigned receipt still fails (exit 1).
            ;;
    esac
    command -v python3 >/dev/null 2>&1 || { printf ''; return 0; }
    python3 -E - "$pj" "$src" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
# The cwd is the checkout being verified: drop it from sys.path before any
# other import, or a committed hashlib.py/json.py could print the verdict.
# The interpreter runs with -E so PYTHONPATH (an empty component would add
# the cwd as an absolute path) and a committed sitecustomize.py cannot act
# before this line.
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, sys, os, hashlib, urllib.request
sys.path.insert(0, sys.argv[3])
try:
    from receipt_jwt import verify_attestation, keyset_problem, check_seal_meta, _CRYPTO_AVAILABLE
except ImportError:
    sys.exit(1)                      # "" : cannot check
try:
    proof = json.load(open(sys.argv[1]))
except Exception:
    sys.exit(1)
tok = (proof.get("verification") or {}).get("attestation")
# `attestation: false` is "no attestation present", the same fact as a missing
# or empty-string field -- never a malformed token. `is False` only, never
# `== False`, because `0 == False` in Python and a numeric attestation field
# is a different (still malformed) shape that must still read as FAILED. Same
# guard in loki_remote_attestation_status above -- change both.
if tok is None or tok == "" or tok is False:
    print("none"); sys.exit(0)       # a FACT about the receipt, not an error
src = sys.argv[2]
try:
    # Scheme check first, same as the bash gate above -- a "-f" test run
    # first would let a local file whose literal name IS an http(s) URL
    # string (the process cwd is the checkout being verified) silently stand
    # in for a real fetch. A recognized scheme always means "URL", even if a
    # same-named file happens to exist on disk.
    if src.startswith(("http://", "https://")):
        url = src if src.rstrip("/").endswith("jwks.json") else \
            src.rstrip("/") + "/.well-known/jwks.json"
        with urllib.request.urlopen(url, timeout=10) as r:
            jwks = json.loads(r.read())
    elif os.path.isfile(src):
        jwks = json.load(open(src))
    else:
        sys.exit(1)
except Exception:
    sys.exit(1)                      # "" : unreachable/unreadable, no verdict
# A key set that is not {"keys": [{...}, ...]} -- INCLUDING an empty list -- is
# a key-set problem: NOT CHECKED, never an accusation. An empty keyset is an
# inability to check (no key was published to check against), not evidence of
# tampering. Checked BEFORE anything below can print "bad", so a bad or empty
# key set (or one that is ambiguous or wrongly tagged for the token's kid, see
# keyset_problem) can never read as FAILED. Same order in loki_remote_attestation_status
# above -- change both.
_keys = jwks.get("keys") if isinstance(jwks, dict) else None
if not isinstance(_keys, list) or not _keys or not all(isinstance(k, dict) for k in _keys):
    sys.exit(1)                      # "" : cannot check
# The receipt's builder controls the token. An attestation that is not a JWT
# string (and not the "no attestation" `false` handled above) is a malformed
# receipt: FAILED, decided without any crypto.
if not isinstance(tok, str):
    print("bad"); sys.exit(0)
# receipt_jwt imports without `cryptography`, and verify_attestation then
# returns False, which would print "bad": a FAILED verdict for a check that
# never ran. A missing verifier dependency is NOT CHECKED. Same guard in
# loki_remote_attestation_status above -- change both.
if not _CRYPTO_AVAILABLE:
    sys.exit(1)                      # "" : cannot check
# A key set that cannot give exactly one usable key for the token's kid is a
# key-set problem, never an accusation: NOT CHECKED, with the reason on stdout.
try:
    _ksp = keyset_problem(tok, jwks)
except Exception:
    _ksp = None
if _ksp:
    print("keyset: " + _ksp); sys.exit(0)
# Any other crash inside verification comes from the token: refuse it.
# Letting it propagate read as NOT CHECKED (exit 2), which let a forger turn
# FAILED into "could not check".
try:
    ok, claims = verify_attestation(tok, jwks)
except Exception:
    print("bad"); sys.exit(0)
if not ok:
    print("bad"); sys.exit(0)
# A verifying signature is not enough: it must attest to THESE bytes. Recompute
# the digest the way the receipt records it, so a token lifted from another run
# cannot vouch for this one.
_vmeta = proof.pop("verification", None)
canon = json.dumps(proof, sort_keys=True, separators=(",", ":")).encode()
if claims.get("receipt_sha256") != hashlib.sha256(canon).hexdigest():
    print("bad"); sys.exit(0)
# seal.v2: the verification.* metadata (scope, algo, attestation_kid, ...) is
# inside the signature; a token that signs different metadata is refused. A
# legacy seal.v1 token still verifies (its metadata is simply unsigned).
print("ok" if check_seal_meta(tok, claims, _vmeta)[0] != "bad" else "bad")
PY
}

# Prints "legacy" when the receipt's attestation is a seal.v1 token (the
# verification.* metadata is NOT inside the signature), else nothing. Callers
# append "metadata not signed (legacy seal.v1)" to a VERIFIED line.
loki_proof_seal_legacy() {
    python3 -E - "$1" "${_LOKI_SCRIPT_DIR}" 2>/dev/null <<'PY' || true
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, sys
sys.path.insert(0, sys.argv[2])
from receipt_jwt import _b64url_decode
try:
    tok = json.load(open(sys.argv[1]))["verification"]["attestation"]
    claims = json.loads(_b64url_decode(tok.split(".")[1]))
    print("legacy" if "seal" not in claims else "")
except Exception:
    print("")
PY
}

# loki_verify_root: the ONE place that decides which tree the receipt verifier
# hashes. The verifier walks and digests the directory it is given, so
# an ambient cwd (a huge checkout, $HOME, a non-project directory) must never be
# the default. Order: explicit TARGET_DIR; the parent of an absolute LOKI_DIR;
# the cwd if it holds .loki; the repo top-level if it holds .loki. Anything else
# prints one plain sentence on stderr and returns 1 (callers report NOT CHECKED).
loki_verify_root() {
    # Helpers live inside so callers that extract this one function stay whole.
    _loki_verify_trim() {
        local p="$1"
        while [ "${#p}" -gt 1 ] && [ "${p%/}" != "$p" ]; do p="${p%/}"; done
        printf '%s' "$p"
    }

    _loki_verify_canon() {
        local p r
        p="$(_loki_verify_trim "$1")"
        if [ -n "$p" ] && [ -d "$p" ] && r="$(cd "$p" 2>/dev/null && pwd -P)"; then p="$r"; fi
        printf '%s' "$p"
    }
    local d="${TARGET_DIR:-}" top="" _cd _ch
    d="$(_loki_verify_trim "$d")"
    if [ -z "$d" ]; then
        case "${LOKI_DIR:-}" in
            /*) [ -d "$LOKI_DIR" ] && d="$(dirname "$LOKI_DIR")" ;;
        esac
    fi
    if [ -z "$d" ]; then
        if [ -d "$PWD/.loki" ]; then
            d="$PWD"
        else
            top="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || true)"
            if [ -n "$top" ] && [ -d "$top/.loki" ]; then d="$top"; fi
        fi
    fi
    # Compare canonical paths: every trailing slash stripped, symlinks resolved,
    # identically for the candidate and HOME (mirrored by resolveVerifyRoot in
    # loki-ts/src/commands/proof.ts).
    _cd="$(_loki_verify_canon "$d")"
    _ch="$(_loki_verify_canon "${HOME:-}")"
    if [ -z "$d" ] || [ "$_cd" = "/" ] || { [ -n "$_ch" ] && [ "$_cd" = "$_ch" ]; }; then
        echo "Cannot tell which project tree to verify against: set TARGET_DIR or run from a project directory that contains .loki." >&2
        return 1
    fi
    printf '%s\n' "$d"
}

loki_remote_verify_receipt() {
    local pj="$1"
    local verifier="${_LOKI_SCRIPT_DIR}/lib/proof-verify.py"

    if [ ! -f "$verifier" ] || ! command -v python3 >/dev/null 2>&1; then
        echo "  NOT VERIFIED: no local verifier available (need python3 and lib/proof-verify.py)" >&2
        return 0
    fi

    # Whether the receipt CARRIES a signature is read from the receipt itself,
    # not inferred from the verifier's gpg verdict. proof-verify.py reports
    # gpg_ok="n/a" for two distinct situations -- no signature present, and gpg
    # unavailable on this machine -- so gpg_ok alone cannot tell them apart, and
    # collapsing them is exactly how a signed receipt gets mislabelled UNSIGNED.
    local signed=0
    if jq -e '.verification.gpg_signature // empty' "$pj" >/dev/null 2>&1; then
        signed=1
    fi

    # Deliberately NOT gated on the verifier's exit code or its `ok` field.
    # `ok` includes diff_drift, re-derived against the LOCAL repo -- which is
    # not the machine that produced this build, so an honest remote receipt
    # drifts by construction and would report TAMPERED on every single run.
    # A false BLOCK is the more damaging direction (docs/VERIFICATION-COST.md):
    # a gate that cries wolf every time trains users to ignore the verdict.
    # Only hash_ok and gpg_ok are load-bearing here; drift is not our verdict.
    local out hash_ok gpg_ok
    # Second argument is the remote URL when this receipt came from a cluster;
    # empty for a local receipt, which simply means no attestation is checked.
    local _attest_url="${2:-}" _attest=""
    _attest="$(loki_remote_attestation_status "$pj" "$_attest_url")"
    local _vroot
    _vroot="$(loki_verify_root)" || { echo "  NOT VERIFIED: no project tree to check against" >&2; return 0; }
    out="$(python3 -I -S "$verifier" "$pj" "$_vroot" 2>/dev/null)"
    hash_ok="$(printf '%s' "$out" | jq -r '.hash_ok // false' 2>/dev/null)"
    gpg_ok="$(printf '%s' "$out" | jq -r '.gpg_ok | tostring' 2>/dev/null)"

    if [ "$hash_ok" != "true" ]; then
        echo -e "  ${RED}TAMPERED: the receipt's integrity hash does not match its contents.${NC}" >&2
        echo "    proof.json was edited after it was written. Do not trust this build." >&2
        return 1
    fi

    # Drift is REPORTED, never a verdict. This receipt was produced in a cluster
    # pod and is being checked against a different tree, so drift is the normal
    # result and says nothing about tampering -- docs/VERIFICATION-COST.md:
    # hash_ok is integrity, `ok` folds in tree_drift and false-alarms here.
    # Stated rather than suppressed: silently dropping a true fact because it is
    # inconvenient is the same error as folding UNCHECKED into UNSIGNED.
    if [ "$(printf '%s' "$out" | jq -r '[.diff_drift,.tree_drift]|index(true)!=null' 2>/dev/null)" = "true" ]; then
        echo "  Drift: this tree differs from the one the receipt records. Expected --"
        echo "    the build ran elsewhere. Not tampering, and not part of the verdict."
    fi

    # An attestation that does not verify is an ACCUSATION, and it is checked
    # before the gpg branches because it is the stronger statement: the server's
    # own published key says this token does not cover these bytes. Either the
    # receipt was altered after the server signed it, or the token was lifted
    # from a different run. Both mean: do not trust this build.
    if [ "$_attest" = "bad" ]; then
        echo -e "  ${RED}TAMPERED: the receipt's attestation does not verify against the server's published keys.${NC}" >&2
        echo "    Either these bytes were altered after signing, or the attestation" >&2
        echo "    belongs to a different run. Do not trust this build." >&2
        return 1
    fi

    # gpg_ok=false covers TWO facts that must never be conflated: the signature
    # is cryptographically bad over this content (the receipt was ALTERED), and
    # gpg could not evaluate it at all (we do not hold the public key). Only the
    # first is an accusation. Reporting a missing key as TAMPERED would send a
    # user hunting a forgery that never happened, and would train them to shrug
    # at a real TAMPERED -- the same error as calling an unsigned receipt
    # verified, just pointed the other way.
    #
    # proof-verify.py returns a bare boolean, so the distinction is drawn here
    # from gpg's own status codes: BADSIG = bad over this content, NO_PUBKEY /
    # ERRSIG = could not be checked.
    if [ "$signed" -eq 1 ] && [ "$gpg_ok" = "false" ]; then
        local _why=""
        if command -v gpg >/dev/null 2>&1; then
            _why="$(loki_remote_gpg_status "$pj")"
        fi
        if [ "$_why" = "nopubkey" ]; then
            # JWKS exists precisely for this case: the gpg key is absent, but a
            # remote-issued receipt can still prove provenance against the
            # server's published key set, with no out-of-band import.
            if [ "$_attest" = "ok" ]; then
                echo -e "  ${GREEN}VERIFIED${NC}: integrity hash matches and the receipt's attestation is valid."
                echo "    Proven: these bytes are unaltered, and the build server signed them"
                echo "    with a key published at ${_attest_url}/.well-known/jwks.json."
                echo "    The gpg key is not in your keyring; the attestation covers provenance."
                echo "    NOT proven: that the code is correct. A receipt records which checks ran."
                return 0
            fi
            echo -e "  ${YELLOW}NOT CHECKED${NC}: the receipt is signed, but the signing key is not in your keyring." >&2
            echo "    Nothing is wrong with the contents: the integrity hash matches. The" >&2
            echo "    signature could not be evaluated, so PROVENANCE is unproven here." >&2
            echo "    Import the publisher's public key and re-run: loki proof verify $(basename "$(dirname "$pj")")" >&2
            # Non-zero: a caller who asked for a signed receipt did not get
            # proof, and in CI that must not pass silently. The message states
            # plainly that no fault was found in the content, so this is a
            # missing-proof exit, not an accusation of tampering.
            return 1
        fi
        echo -e "  ${RED}TAMPERED: the receipt carries a gpg signature that does not verify.${NC}" >&2
        echo "    Either the contents or the signature was altered. Do not trust this build." >&2
        return 1
    fi

    if [ "$signed" -eq 1 ] && [ "$gpg_ok" = "true" ]; then
        echo -e "  ${GREEN}VERIFIED${NC}: integrity hash matches and the gpg signature is valid."
        echo "    Proven: these receipt bytes are unaltered, and they were signed by the"
        echo "    holder of the signing key. Verify that key is one you trust."
        echo "    NOT proven: that the code is correct. A receipt records which checks ran."
        return 0
    fi

    if [ "$signed" -eq 1 ]; then
        # gpg_ok is "n/a" with a signature present: gpg is missing here.
        echo -e "  ${YELLOW}NOT CHECKED${NC}: the receipt is signed, but gpg is unavailable on this machine."
        echo "    Its integrity hash matches. The signature was NOT checked, so provenance"
        echo "    is unproven here. Install gpg and re-run: loki proof verify $(basename "$(dirname "$pj")")"
        return 0
    fi

    # No gpg signature, but a verified attestation. This is the normal shape for
    # a cluster build: the receiver signs, the build never holds a gpg key, and
    # provenance is still proven -- so calling it UNSIGNED would understate what
    # was actually checked, the mirror of the error we refuse in the other
    # direction.
    if [ "$_attest" = "ok" ]; then
        echo -e "  ${GREEN}VERIFIED${NC}: integrity hash matches and the receipt's attestation is valid."
        echo "    Proven: these bytes are unaltered, and they were signed by the build"
        echo "    server whose key is published at ${_attest_url}/.well-known/jwks.json."
        echo "    Verify that server is one you trust."
        echo "    NOT proven: that the code is correct. A receipt records which checks ran."
        return 0
    fi

    # The receipt carries an attestation that could not be checked here (JWKS
    # unreachable, or python cryptography missing). Calling it UNSIGNED would
    # mislabel a signed receipt and point at the wrong fix. Same policy as the
    # gpg-unavailable branch above: NOT CHECKED, return 0, provenance unproven.
    if [ -z "$_attest" ] && jq -e '.verification.attestation // empty' "$pj" >/dev/null 2>&1; then
        echo -e "  ${YELLOW}NOT CHECKED${NC}: the receipt carries an attestation, but it could not be checked"
        echo "    against ${_attest_url:-<no url>} (JWKS unreachable, python cryptography missing, or"
        echo "    the JWKS URL is not https -- only loopback is allowed over plain http)."
        echo "    Its integrity hash matches. Provenance is unproven here, not disproven."
        echo "    Install python3 cryptography, or re-run where the build server is reachable."
        echo "    The verifier runs python3 -E, so cryptography supplied through PYTHONPATH is"
        echo "    not seen; install it where the base interpreter finds it (site or user site)."
        return 0
    fi

    echo -e "  ${YELLOW}UNSIGNED${NC}: integrity hash matches. This is NOT proof of provenance."
    echo "    Proven: the receipt is internally consistent with its own hash."
    echo "    NOT proven: who produced it. An unsigned receipt is forgeable -- whoever"
    echo "    controls the builder can rewrite the facts and recompute the hash."
    echo "    For provenance, sign the build (docs/SIGNED-RECEIPTS.md)."
    return 0
}

# Emit event (non-blocking)
# Usage: emit_event <type> <source> <action> [key=value ...]
emit_event() {
    if [ -f "$EMIT_SH" ]; then
        # Run in background to be non-blocking
        ("$EMIT_SH" "$@" >/dev/null 2>&1 &)
    fi
}

# --- CLI consolidation (Phase A): deprecated-alias back-compat contract -------
# An old command token continues to dispatch and forward 1:1 to its canonical
# command. It prints exactly ONE pointer line to STDERR and never touches
# machine-readable stdout. The deprecation line is suppressed whenever an
# explicit machine-output flag (--json, -q, --quiet) is present in the args, so
# callers that capture combined 2>&1 (and JSON consumers) see a clean stream.
# It is ALSO suppressed for the positional machine-output formats of forwarded
# commands (json|csv|timeline as the first arg, e.g. `loki export json`), since
# those formats emit a pure machine-readable stream that a combined 2>&1 capture
# would otherwise dirty with the note line. The human-readable `markdown` format
# is intentionally NOT suppressed.
# Non-TTY stdout is intentionally NOT a suppression trigger (some callers pipe
# stdout but read stderr).
#
# Usage:
#   _deprecated_alias <old> <new> "$@"   # emit pointer + telemetry (suppressed
#                                        # under --json/-q/--quiet); then the
#                                        # caller forwards to the canonical cmd.
# Reference implementation: the run->start deprecation (cmd_run / v6.84.0).
_deprecated_alias_should_suppress() {
    # Returns 0 (suppress) when a machine-output flag is present anywhere in
    # "$@", OR when the FIRST arg is a positional machine-output format
    # (json|csv|timeline) used by forwarded commands like `loki export json`.
    # The positional check is first-arg-only so we never suppress on an
    # incidental later token; markdown (human-readable) is deliberately excluded.
    case "${1:-}" in
        json|csv|timeline) return 0 ;;
    esac
    local a
    for a in "$@"; do
        case "$a" in
            --json|-q|--quiet) return 0 ;;
        esac
    done
    return 1
}

_deprecated_alias() {
    local old="$1"; shift
    local new="$1"; shift
    # "$@" is now the user-supplied argv for the alias invocation.
    if _deprecated_alias_should_suppress "$@"; then
        return 0
    fi
    echo "note: 'loki ${old}' is now 'loki ${new}'. The old form still works." >&2
    # Reuse the existing cli_command_deprecated telemetry event (from=/to=).
    # IMPORTANT: a forwarding alias must add no side effect the canonical command
    # lacks. events/emit.sh creates "$LOKI_DIR/events/pending" as a side effect,
    # which would make a no-session directory suddenly contain .loki and flip the
    # exit code of downstream guards (e.g. cmd_share's "No .loki/ directory"
    # check). It also removes a bash-vs-bun race for read-only reporters
    # (cmd_stats sees .loki appear vs Bun seeing it absent). So we only emit
    # telemetry when .loki already exists; the stderr pointer line is always
    # printed (modulo the machine-output suppression above). The matching Bun
    # helper (loki-ts/src/util/deprecated_alias.ts) emits no telemetry, so the
    # two routes are side-effect consistent.
    if [ -d "${LOKI_DIR:-.loki}" ]; then
        emit_event cli_command_deprecated cli alias \
            "from=${old}" \
            "to=${new}" \
            "argv=${*:-}"
    fi
}

# CLI consolidation (Phase B): kpis is a Bun-only command (Phase K, v7.5.28+).
# It ships in the Bun runtime (loki-ts) and reuses the canonical cost arithmetic
# in loki-ts/src/runner/budget.ts (PRICING map + calculateCostFromRecords, the
# cost single-source-of-truth). It is deliberately NOT reimplemented in bash:
# a bash port would duplicate that arithmetic and risk drift. On the bash route
# (LOKI_LEGACY_BASH=1, or a machine without Bun) both the canonical
# `report kpis` and the deprecated `kpis` alias print this honest requirement
# message rather than the generic "Unknown command" (which would contradict
# `loki help` listing kpis). --json yields a structured {"available": false}
# payload so a machine consumer is not handed prose. Exits 1.
# Usage: _kpis_bash_unavailable "$@"   (the alias arm prints the pointer first)
# --json is honored FLAG-ANYWHERE (not just as $1): a machine consumer that
# passes the suppression flags first -- e.g. `kpis --quiet --json` or `report
# kpis -q --json` -- must still get the structured {"available": false} payload
# on stdout, not prose. This mirrors _deprecated_alias_should_suppress, which
# also scans all of "$@". Exit 1 either way (the bash route never produces KPIs).
# Copy points at the CANONICAL `loki report kpis` (the deprecated `loki kpis`
# alias prints its own pointer first, at the alias arm).
_kpis_bash_unavailable() {
    local _ka _want_json=""
    for _ka in "$@"; do
        if [ "$_ka" = "--json" ]; then
            _want_json=1
            break
        fi
    done
    if [ -n "$_want_json" ]; then
        printf '{"available": false, "reason": "loki report kpis requires the Bun runtime; it is not implemented on the bash (LOKI_LEGACY_BASH) route"}\n'
    else
        echo "loki report kpis requires the Bun runtime and is not available on the bash route." >&2
        echo "Remove LOKI_LEGACY_BASH=1 (or install Bun: https://bun.sh) to use 'loki report kpis'." >&2
    fi
    exit 1
}

# Emit learning signal (non-blocking) - SYN-018
# Usage: emit_learning_signal <signal_type> [options]
# Signal types: user_preference, error_pattern, success_pattern, tool_efficiency, workflow_pattern
# See learning/emit.sh for full option documentation
emit_learning_signal() {
    if [ -f "$LEARNING_EMIT_SH" ]; then
        # Run in background to be non-blocking
        (LOKI_DIR="$LOKI_DIR" LOKI_SKILL_DIR="$SKILL_DIR" "$LEARNING_EMIT_SH" "$@" >/dev/null 2>&1 &)
    fi
}

# Track session timing for efficiency signals
SESSION_START_TIME=""
record_session_start() {
    SESSION_START_TIME=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
}

# Calculate session duration in milliseconds
get_session_duration_ms() {
    if [ -n "$SESSION_START_TIME" ]; then
        local end_time
        end_time=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
        echo $((end_time - SESSION_START_TIME))
    else
        echo "0"
    fi
}

# Load memory context at startup (SYN-008)
# Retrieves task-aware memories and writes to .loki/state/memory-context.json
# Note: Uses different file than get_relevant_learnings() in run.sh which writes to relevant-learnings.json
# Usage: load_memory_context [prd_path]
load_memory_context() {
    local prd_path="${1:-}"
    local output_file="$LOKI_DIR/state/memory-context.json"
    local temp_file="$LOKI_DIR/state/.memory-context.json.tmp"

    # Skip if explicitly disabled
    if [[ "${LOKI_SKIP_MEMORY:-}" == "true" ]]; then
        return 0
    fi

    # v7.7.23 privacy opt-out (excellence bar 6): honor a per-project
    # .loki/config.json {"memory": {"disabled": true}} flag. Lets a user
    # disable ALL memory capture/retrieval for a sensitive project without
    # setting an env var on every invocation.
    if [ -f "$LOKI_DIR/config.json" ]; then
        # v7.7.23 council fix (Opus 2): FAIL-CLOSED. A config.json that
        # exists but cannot be parsed prints 'true' (suppress memory) so
        # a JSON typo on a sensitive project does not silently re-enable
        # retrieval. Only the no-config case (outer -f guard false)
        # proceeds with memory on.
        local _mem_disabled
        _mem_disabled=$(python3 -c "
import json, sys
try:
    d = json.load(open('$LOKI_DIR/config.json'))
    print('true' if d.get('memory', {}).get('disabled') is True else 'false')
except Exception:
    print('true')  # malformed config -> fail closed (suppress memory)
" 2>/dev/null || echo "true")
        if [ "$_mem_disabled" = "true" ]; then
            return 0
        fi
    fi

    # Check if python3 is available (required for memory system)
    if ! command -v python3 &> /dev/null; then
        echo -e "${YELLOW}Warning: python3 not found - memory context loading disabled${NC}" >&2
        return 0
    fi

    # Check if memory system is available
    if [ ! -d "$LOKI_DIR/memory" ]; then
        return 0
    fi

    # Ensure state directory exists
    mkdir -p "$LOKI_DIR/state"

    # Extract context from PRD if available (first 500 chars)
    # Use base64 encoding to safely pass PRD content to Python (prevents command injection)
    local prd_context_b64=""
    if [ -n "$prd_path" ] && [ -f "$prd_path" ]; then
        prd_context_b64=$(head -c 500 "$prd_path" 2>/dev/null | base64)
    fi

    # Call Python memory retrieval with safe parameter passing via environment variables
    # Export environment variables for Python script (cleaner than shell variable expansion)
    local result
    _LOKI_SKILL_DIR="$SKILL_DIR" _LOKI_DIR="$LOKI_DIR" _LOKI_PRD_CONTEXT_B64="$prd_context_b64" \
    result=$(python3 << 'PYEOF' 2>/dev/null
import sys
import os
import json
import base64

# Get parameters from environment
skill_dir = os.environ.get('_LOKI_SKILL_DIR', '')
loki_dir = os.environ.get('_LOKI_DIR', '')
prd_context_b64 = os.environ.get('_LOKI_PRD_CONTEXT_B64', '')

# Decode PRD context safely
prd_context = ''
if prd_context_b64:
    try:
        prd_context = base64.b64decode(prd_context_b64).decode('utf-8', errors='replace')
    except Exception:
        prd_context = ''

sys.path.insert(0, skill_dir)
try:
    from memory.retrieval import MemoryRetrieval
    from memory.storage import MemoryStorage

    storage = MemoryStorage(f'{loki_dir}/memory')
    retriever = MemoryRetrieval(storage)

    # Build context for task-aware retrieval
    context = {
        'goal': prd_context if prd_context else 'Analyze codebase and improve',
        'phase': 'startup'
    }

    # Retrieve relevant memories
    results = retriever.retrieve_task_aware(context, top_k=5, token_budget=2000)

    # Format output
    output = {
        'timestamp': __import__('datetime').datetime.now().isoformat(),
        'prd_context': prd_context[:200] if prd_context else None,
        'memory_count': len(results),
        'memories': []
    }

    for r in results:
        memory_item = {
            'source': r.get('_source', 'unknown'),
            'score': round(r.get('_weighted_score', r.get('_score', 0)), 3),
            'summary': r.get('summary', r.get('pattern', r.get('goal', '')))[:200],
            'id': r.get('id', '')
        }
        output['memories'].append(memory_item)

    # v7.7.20: wake the previously-dead cross-project + knowledge-graph
    # code. When local episodic retrieval is sparse (< 5 hits), augment
    # with patterns mined from OTHER projects' .loki/memory/ stores. This
    # is the cross-project learning the diagnosis flagged as dead code
    # (zero call sites). Best-effort: any failure is swallowed and the
    # local results stand alone. Opt out with LOKI_SKIP_CROSS_PROJECT=true.
    output['cross_project'] = []
    if os.environ.get('LOKI_SKIP_CROSS_PROJECT', '').lower() not in ('true', '1', 'yes'):
        try:
            from memory.knowledge_graph import OrganizationKnowledgeGraph
            kg = OrganizationKnowledgeGraph()
            query_text = context.get('goal', '') or 'general'
            patterns = kg.query_patterns(query_text, max_results=3)
            for p in patterns:
                output['cross_project'].append({
                    'source': 'knowledge-graph',
                    'pattern': str(p.get('description', p.get('pattern', p)))[:200]
                              if isinstance(p, dict) else str(p)[:200],
                    'score': round(float(p.get('score', 0.0)), 3) if isinstance(p, dict) else 0.0,
                })
        except Exception:
            pass  # cross-project augmentation is best-effort
    output['cross_project_count'] = len(output['cross_project'])

    print(json.dumps(output, indent=2))
except ImportError:
    print('{"error": "Memory module not available", "memory_count": 0, "memories": []}')
except Exception as e:
    print(json.dumps({"error": str(e), "memory_count": 0, "memories": []}))
PYEOF
    )

    # Write to output file using atomic write (temp file + rename)
    if [ -n "$result" ]; then
        echo "$result" > "$temp_file" && mv "$temp_file" "$output_file"

        # Emit event
        local memory_count=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('memory_count', 0))" 2>/dev/null || echo "0")
        emit_event memory cli load "count=$memory_count" "prd_provided=$([[ -n "$prd_path" ]] && echo 'true' || echo 'false')"

        # Print summary
        if [ "$memory_count" -gt 0 ]; then
            echo -e "${CYAN}Memory:${NC} Loaded $memory_count relevant learnings"
        fi
    else
        # Write empty result using atomic write
        echo '{"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", "memory_count": 0, "memories": [], "error": "retrieval_failed"}' > "$temp_file" && mv "$temp_file" "$output_file"
    fi

    return 0
}

# Show help
# `loki help answer`: `loki answer` is a v10 (loki-ts) command with no bash
# implementation, so its help lives here rather than behind delegation.
_help_answer() {
    echo -e "${BOLD}loki answer${NC} - Answer a BLOCKED run; it continues under the same run id"
    echo ""
    echo "Usage: loki answer [<run-id>] [\"<text>\" | --text \"...\"]"
    echo ""
    echo "A run BLOCKED on a spec conflict continues in place under the same"
    echo "run id: the answer joins the locked task, stages that already settled"
    echo "are restored and not re-run, and the log records run.resumed."
    echo ""
    echo "Arguments:"
    echo "  <run-id>        BLOCKED run to answer. Default: the newest BLOCKED run"
    echo "                  in the current repo (else the latest run)."
    echo ""
    echo "Options:"
    echo "  \"<text>\"       The answer text, as a positional argument (or --text \"...\","
    echo "  --text \"...\"    --text=...). Without either, the answer is"
    echo "                  read from the Control Plane answer file"
    echo "                  \$LOKI_CONTROL_ANSWER_DIR/<source>/<run-id>.answer.txt"
    echo "                  (default dir: ~/.loki/control/answers)."
    echo ""
    echo "Exit codes: 0 the new run succeeded (otherwise its exit code), 2 no runs,"
    echo "run not BLOCKED, no answer supplied, or bad usage."
    echo "The child run drops SLACK_* and LOKI_CONTROL_TOKEN from its environment"
    echo "and sets LOKI_NO_BROWSER=1."
}

# `loki help verify-pr`: a Bun-only command (loki-ts), so its help lives here.
_help_verify_pr() {
    echo -e "${BOLD}loki verify-pr${NC} - Verify a PR against its linked issue (experimental, LOKI_VERIFY_PR=1)"
    echo ""
    echo "Usage: loki verify-pr <https://github.com/o/r/pull/N | o/r#N> [--out DIR] [--json]"
    echo "       loki verify-pr respond <o/r#N>   fix open review comments (one commit each, on the PR"
    echo "                                        branch), push, reply with the sha; never resolves threads"
    echo ""
    echo "Derives a check from the PR's linked issue (Check: \`cmd\` or a loki-check fence)."
    echo "VERIFIED only if the check fails on the PR base, passes on its head, and the"
    echo "touched packages' suites pass. A PR with no linked issue, no check, a removed"
    echo "test or a non-discriminating check is NOT PROVEN. Writes verify-pr-result.json;"
    echo "--json also prints that verdict object on stdout instead of the text summary."
    echo "PR code runs only in the container sandbox (scripts/verify-pr-sandbox.sh); set"
    echo "LOKI_VPR_IMAGE to a locally present image."
    echo ""
    echo "Exit codes: 0 verified, 2 usage or sandbox blocked, 3 no proof to run (no linked issue or no trusted runnable check), 4 proof ran and failed."
}

# Appended to `loki help verify`: the v10 DSSE export flag (docs/exit-codes.md).
_help_verify_dsse() {
    echo ""
    echo "DSSE EXPORT (v10 engine):"
    echo "    loki verify <run-id|receipt.json> --export-dsse"
    echo "        Print a VERIFIED or ALREADY_SATISFIED run receipt as an in-toto"
    echo "        Statement v1 inside a DSSE envelope (Ed25519 over PAE). Refusals"
    echo "        print nothing on stdout. Exit codes:"
    echo "          0   envelope printed"
    echo "          1   TAMPERED, an envelope given as input, or run_id mismatch"
    echo "          2   UNSIGNED, UNCHECKED, or unreadable receipt"
    echo "          4   run outcome is not VERIFIED or ALREADY_SATISFIED"
    echo "          66  no signing key, or no runs"
}

# Default help screen: core verbs only. `loki help all` prints show_help_all.
show_help() {
    # shellcheck source=/dev/null
    source "$_LOKI_SCRIPT_DIR/lib/help-core.sh"
    help_core_screen "$(get_version)"
}

show_help_all() {
    local version=$(get_version)
    # Strip color when stdout is not a TTY (piped/redirected) so captured help
    # output is clean, matching the bare-loki welcome path. Local overrides only.
    local BOLD="$BOLD" NC="$NC"
    if [ ! -t 1 ]; then
        BOLD=''; NC=''
    fi
    echo -e "${BOLD}Loki Mode v$version${NC}"
    echo ""
    echo "Usage: loki <command> [options]"
    echo ""
    echo "New here? Try these in order:"
    echo "  loki setup                One-step wizard: tools, provider login, model route, spend cap (--yes for defaults)"
    echo "  loki welcome              Friendly first-run opener (what Loki is, next steps)"
    echo "  loki doctor               Check your setup is ready (a few seconds)"
    echo "  loki quickstart           Guided first build from your idea (no PRD needed)"
    echo "  loki start ./prd.md       Build from a spec (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
    echo "  quickstart = guided first build; quick = one small task (Loki 10 engine, no PR)."
    echo "  loki \"<task>\", loki owner/repo#N and loki quick run on the Loki 10 engine by default."
    echo "  Docs: https://github.com/asklokesh/loki-mode  |  Report a problem: loki crash"
    echo ""
    # CLI consolidation (Phase A): the front page presents ~17 canonical
    # workflow-oriented entries grouped by section. Deprecated alias names
    # (stats, metrics, cost, trust-metrics, serve, open, otel, cp, wt, rc, ...)
    # do NOT appear here; they live in the collapsed footer below and in
    # `loki help aliases`. The full long-tail surface is still dispatchable and
    # documented per-command via `loki <command> --help`.
    #
    # v7.39.0: trimmed three lower-traffic canonical entries off the front page
    # to hold the lean target after `ultracode` (v7.38.0) joined Verify/trust:
    # grill (pre-build spec interrogation, advanced), spec (living-spec drift,
    # advanced), and cleanup (orphaned-process recovery, rare). They remain
    # fully canonical (NOT aliases), dispatchable, and listed in the "More
    # commands" footer below + documented via `loki <command> --help`.
    echo "Commands:"
    echo ""
    echo "Build:"
    echo "  start [SPEC]     Start a build (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
    echo "  plan <PRD-file>  Dry-run analysis: complexity, cost, execution plan"
    echo "  backlog <owner/repo> --all|--label X|--issues N,N   Run every matching open issue, N in parallel"
    echo "  workspace list | infer <name> | run <name> <ref>   Multi-repo runs from loki.yaml workspaces (LOKI_WORKSPACES=0 disables)"
    echo "  start --workspace <name> \"<brief>\"   Run a workspace; start --deploy [--health-url U] deploys after a VERIFIED run"
    echo "  quickstart [idea]  Guided first build: setup, idea, template, plan, go"
    echo "  try <repo-url>     Try Loki on a public repo in a temp clone (--dry-run previews); only the receipt is kept"
    echo ""
    echo "Session:"
    echo "  status [--json|--badge|--probe]  Show current status (--json machine-readable, --badge deploy badge, --probe re-check deploy health)"
    echo "  next             Run the right next step for you (resume / ship / why)"
    echo "  stop             Stop execution immediately"
    echo "  pause            Pause after current session"
    echo "  resume           Resume a paused or interrupted run (after Ctrl-C or a crash)"
    echo "  cancel           Cancel the active Loki 10 run at its next stage boundary"
    echo "  steer \"<note>\"    Nudge a running build (Loki 10 engine; legacy loop needs LOKI_PROMPT_INJECTION=true)"
    echo ""
    echo "Verify / trust:"
    echo "  verify [base] [--fast]  Deterministic PR verification (CI-gate exit codes; --fast: no model, no network)"
    echo "  verify-pr <pr>   Verify a PR against its linked issue; 'verify-pr respond <pr>' fixes review comments"
    echo "  keys export     Print the public receipt-signing key (JWK) for third-party verify --pubkey"
    echo "  review [opts]    Standalone code review with quality gates"
    echo "  ultracode \"task\" Run a task as a native Claude Code Dynamic Workflow (opt-in, Claude-only)"
    echo "  trust [cmd]      Visible trust trajectory; 'trust detail' for metrics"
    echo "  undo <run> [--plan|--dry-run]  Plan or apply (LOKI_UNDO=1) an undo of a run; verifies the receipt"
    echo "  undo --list                    List recent runs with verdict, files changed and age"
    echo ""
    echo "Observe:"
    echo "  dashboard [cmd]  Operations UI server (start|stop|status|url|open)"
    echo "  mcp              Launch the MCP server (Model Context Protocol, stdio)"
    echo "  acp              Run Loki as an ACP agent over stdio (VS Code, JetBrains, Zed)"
    echo ""
    echo "Report:"
    echo "  report [cmd]     Reporting: session|metrics|cost|export|share|dogfood|release-notes"
    echo ""
    echo "Knowledge:"
    echo "  analyze [cmd]    Codebase intelligence: explain|onboard|code|context"
    echo "  memory [cmd]     Cross-project learnings (list|show|search|stats|compound)"
    echo ""
    echo "Modernize:"
    echo "  modernize [cmd]  Legacy modernization: heal|migrate"
    echo ""
    echo "Config:"
    echo "  config [cmd]     Manage configuration (show|init|edit|path|set|get)"
    echo "  doctor [--json] [--quiet]  Check system prerequisites and skill symlinks"
    echo ""
    echo "  version [--json] Show version (--json for machine-readable)"
    echo "  help             Show this help ('loki help aliases' for old names)"
    echo ""
    echo "More commands (the most useful; many more are dispatchable):"
    echo "  Advanced:    ship, deploy, init, web"
    echo "  Cloud + CI:  github, import, ci"
    echo "  Debugging:   logs, crash, cleanup"
    echo "...run 'loki help <name>' for any command."
    echo ""
    echo "Aliases (deprecated): older command names still work; they print a"
    echo "one-line pointer to stderr and never alter --json output. See the full"
    echo "list with 'loki help aliases'."
    echo ""
    echo "Options for 'start':"
    echo "  --provider NAME  AI provider: claude (default), codex, cline, aider, opencode"
    echo "  (model route: set LOKI_MODEL_ROUTE, e.g. ollama/qwen3-coder; there is no --model flag)"
    echo "  --simple         Force simple complexity tier (3 phases)"
    echo "  --complex        Force complex complexity tier (8 phases)"
    echo "  --no-dashboard   Disable web dashboard"
    echo "  --isolation LVL  Per-run isolation dial: none (default) | worktree | docker"
    echo "  --skip-memory    Skip loading memory context at startup"
    echo "  --cost PROFILE   Opt-in cost profile: lean sets LOKI_IMPLEMENT_HAIKU_FIRST=1, LOKI_E10_TRIM=1, LOKI_E10_SMALL_NO_RUNNER=1 and LOKI_E10_SMALL_MAX_CHARS=1200 unless already set (also LOKI_COST_PROFILE)"
    echo "  --spec FILE      Run against an edited spec (written by 'loki plan <task> --spec'; see also --spec-first)"
    echo "  --fresh-prd      Regenerate the PRD from the codebase (no-PRD runs; ignores the reusable generated PRD)"
    echo "  --compliance PRESET  Enable compliance mode (default|healthcare|fintech|government)"
    echo "  --budget USD     Per-run cost cap in USD (default $(loki_budget_default_usd); auto-pause when exceeded; see docs/cost-controls.md)"
    echo "  --bmad-project PATH  Use BMAD Method project artifacts as input"
    echo "  --openspec PATH      Use OpenSpec change directory as input"
    echo "  --config PATH    Load settings from a config file (.yaml/.json/.env). Alias: --env-file, --vars"
    echo "                   Precedence: CLI flag > env var > --config file > defaults. Secrets via \${VAR}."
    echo "  --deploy         After a start that exits 0, run the gated loki deploy --execute. Deploy flags: --destination preview|staging|production,"
    echo "                   --canary, --env-file FILE (deploy env, not config, with --deploy), --domain HOST, --github (publish after deploy),"
    echo "                   --health-url URL, --health-match STR, --auto-rollback"
    echo ""
    echo "Deprecated alias 'run': 'loki run <issue-ref>' still works as an alias"
    echo "for 'loki start'. See its options with 'loki run --help'."
    echo ""
    echo "Examples:"
    echo ""
    echo "  # Starting a session"
    echo "  loki start ./prd.md           # Start with PRD file"
    echo "  loki start owner/repo#123     # GitHub issue (auto-fetches)"
    echo "  loki start PROJ-456           # Jira issue"
    echo "  loki quick \"add dark mode\"  # Single-task mode (Loki 10 engine)"
    echo "  loki tour                     # See a real sample result (no provider, no key, no spend)"
    echo "  loki tour --live              # Same as loki wow: a tiny real task when a provider is ready"
    echo "  loki wow                      # Live mini-task, or the offline sample when no key is available"
    echo "  loki factory \"<task>\"         # loki start, and print the autonomy policy"
    echo "  loki demo                     # Build a sample todo app end to end"
    echo "  loki init -t saas-starter     # Scaffold from template"
    echo "  loki template install <src>   # Install a community PRD template"
    echo ""
    echo "  # Session ops + observability"
    echo "  loki status [--json]          # Current status"
    echo "  loki report session --efficiency   # Token + cost stats"
    echo "  loki report kpis [--json]     # Accuracy + efficiency KPI snapshot"
    echo "  loki report kpis --since 7d   # Window KPI totals (Nd or YYYY-MM-DD)"
    echo "  loki doctor [--json]          # System prereq + skill symlinks"
    echo "  loki logs                     # Tail recent log output"
    echo "  loki report export json|markdown|csv|timeline   # Export session"
    echo "  loki assets export team.tgz   # Export shareable team assets (redacted)"
    echo "  loki cleanup                  # Kill orphaned processes"
    echo ""
    echo "  # Providers + model routing"
    echo "  loki provider list            # Show available providers, including opencode"
    echo "  loki provider set codex       # Switch active provider"
    echo "  # Cheap / open models (DeepSeek, GLM, MiniMax, Kimi) via OpenRouter:"
    echo "  loki provider set aider       # aider + cline reach any OpenRouter model"
    echo "  export OPENROUTER_API_KEY=sk-or-..."
    echo "  loki start ./prd.md           # defaults to deepseek-v3.2 (open weights)"
    echo "  # Pick a specific model:"
    echo "  export LOKI_AIDER_MODEL=openrouter/z-ai/glm-4.6"
    echo ""
    echo "  # Anthropic-PROTOCOL gateways (LiteLLM, Bedrock proxies, self-hosted):"
    echo "  export ANTHROPIC_BASE_URL=https://your-gateway/v1 \\\\"
    echo "         LOKI_MODEL_OVERRIDE=<model-id-your-gateway-serves>"
    echo "  # NOTE: this routes Claude Code itself, so the endpoint must speak the"
    echo "  #       Anthropic /v1/messages API. OpenRouter does NOT -- it serves"
    echo "  #       only the OpenAI-shaped /v1/chat/completions. Use the aider or"
    echo "  #       cline route above for OpenRouter."
    echo ""
    echo "  # Cross-project context (Phase F)"
    echo "  # Drop .loki/app.json with {schema_version:1,app_id:myapp,members:[ui,api]}"
    echo "  # at the parent of related repos for shared CLAUDE.md + memory."
    echo ""
    echo "  # Memory + learnings"
    echo "  loki memory list              # All learnings"
    echo "  loki memory search <query>    # Search across learnings"
    echo "  loki memory consolidate       # Run episodic-to-semantic pipeline"
    echo "  loki memory learn --recent N  # Learn from the newest N merged PRs"
    echo ""
    echo "  # Config + dashboard"
    echo "  loki config set maxTier sonnet   # Cap model cost"
    echo "  loki dashboard start          # Open the Control Plane UI"
    echo "  loki watch                    # Auto-rerun on PRD changes"
    echo "  loki cockpit                  # Live multi-repo cockpit (terminal image)"
    echo "  loki remote                   # Remote session (phone/browser)"
    echo ""
    # COMPLETE COMMAND INDEX. The curated sections above stay curated -- they
    # are the onboarding path and stuffing 112 commands into them would make
    # the first screen useless. But a command that appears NOWHERE in help does
    # not exist to a user: measured, 43 of 112 were unreachable from here,
    # including `loki proof` -- the Evidence Receipt, which is the whole trust
    # argument. Discoverability is an adoption lever, not tidiness.
    #
    # tests/test-help-discoverability.sh fails the build when a real command is
    # missing from this list, so a new command cannot ship invisible.
    echo ""
    echo "All commands:"
    echo "  agents chat diff edit models route"
    echo "  acp agent analyze api assets audit backlog backup bench cancel checkpoint (cp) ci cleanup"
    echo "  cluster cockpit code compliance completions compound config context (ctx)"
    echo "  cost council crash dashboard demo deploy docker docs doctor dogfood"
    echo "  enterprise estimate explain export factory failover github grill heal help"
    echo "  import init intent"
    echo "  issue keys kpis logs magic mcp memory metrics migrate modernize monitor"
    echo "  next notify onboard open optimize otel outcomes own (handoff) pause plan preview"
    echo "  projects proof (receipt) provider quick quickstart rc remote report reset"
    echo "  merge restore resume review rollback run runs sandbox secrets secure self-update sentrux"
    echo "  serve setup setup-skill share ship spec start state stats status steer stop"
    echo "  syslog telemetry template test tour trigger trust trust-metrics try"
    echo "  ultracode undo update verify version voice watch watchdog web welcome why"
    echo "  wiki workspace worktree (wt) wow"
    echo "  control"
    echo ""
    echo "Any command: loki <command> --help"
    echo ""
    echo "Phase A-J features are default-on. See CHANGELOG."
    echo ""
    echo "Environment variables an operator would set: docs/environment-variables.md"
    echo "Cost controls (budget, iteration and wall-clock caps): docs/cost-controls.md"
    echo "Already have a codebase? Start read-only: docs/brownfield.md"
    echo "No egress allowed? What runs disconnected: docs/air-gapped.md"
    echo ""
    echo "Scripting Loki? Exit codes for every command are documented in"
    echo "docs/exit-codes.md. Severity rises with the code, and 'loki start'"
    echo "has a richer contract under LOKI_DURABLE_STATE=1 for k8s and ECS."
    echo ""
    echo "Environment Variables:"
    echo "  Opt-outs: LOKI_HOOK_EVENTS=off, LOKI_DYNAMIC_PROMPT_SECTIONS=keep,"
    echo "  LOKI_MEMORY_BASE_PATH (shared memory dir for app graph)"
    echo "  See: $RUN_SH (header comments) for full list."
}

# CLI consolidation (Phase A): `loki help aliases` subview. Lists every
# deprecated alias and its canonical command for users with scripts. The old
# forms continue to work (they print a one-line stderr pointer and never alter
# --json output); they are supported through at least two MAJOR versions and
# considered for removal no earlier than vN+2.0.0.
show_help_aliases() {
    # Strip color when stdout is not a TTY (piped/redirected), matching the
    # bare-loki welcome path and show_help. Local overrides only.
    local BOLD="$BOLD" NC="$NC"
    if [ ! -t 1 ]; then
        BOLD=''; NC=''
    fi
    echo -e "${BOLD}Loki Mode -- deprecated command aliases${NC}"
    echo ""
    echo "These older command names still work. Each forwards 1:1 to its"
    echo "canonical command, prints exactly one pointer line to stderr, and"
    echo "never alters --json output (the pointer is suppressed under --json,"
    echo "-q, and --quiet). They are supported through at least two MAJOR"
    echo "versions; removal no earlier than vN+2.0.0."
    echo ""
    echo "  Old command        Canonical command"
    echo "  -----------        -----------------"
    echo "  run <issue>        start <issue>"
    echo "  serve              api start"
    echo "  open               preview"
    echo "  otel               telemetry"
    echo "  cp                 checkpoint"
    echo "  wt                 worktree"
    echo "  rc                 remote"
    echo "  stats              report session"
    echo "  metrics            report metrics"
    echo "  cost               report cost"
    echo "  export             report export"
    echo "  share              report share"
    echo "  dogfood            report dogfood"
    echo "  kpis               report kpis"
    echo "  trust-metrics      trust detail"
    echo "  compound           memory compound"
    echo "  explain            analyze explain"
    echo "  onboard            analyze onboard"
    echo "  code               analyze code"
    echo "  context            analyze context"
    echo "  ctx                analyze context"
    echo "  heal               modernize heal"
    echo "  migrate            modernize migrate"
    echo ""
    echo "More groupings (ui, new, admin) land in later Phase B slices."
    echo "See internal/CLI-CONSOLIDATION-DESIGN.md for the full migration plan."
}

# bare `loki`. Reuse a running dashboard, else start one on a free
# loopback port, then open /start (or just print the URL when headless).
loki_ui_free_port() {
    python3 - "$1" <<'PY'
import socket, sys
for p in range(int(sys.argv[1]), int(sys.argv[1]) + 100):
    s = socket.socket()
    try:
        s.bind(("127.0.0.1", p))
    except OSError:
        continue
    finally:
        s.close()
    print(p)
    break
PY
}

# Fail-closed reuse check for a dashboard found via the ~/.loki/dashboard
# registry or a busy port: reusable only if /health reports THIS package's
# version AND realpath, and GET / is 200 text/html. Any missing field, curl
# error or mismatch means "not ours". Mirrors _loki_dashboard_reusable in run.sh.
_loki_dashboard_port_is_ours() {
    local port="$1" want want_pkg health probe
    case "$port" in ''|*[!0-9]*) return 1 ;; esac
    want="$(tr -d '[:space:]' <"$SKILL_DIR/VERSION" 2>/dev/null)" || return 1
    [ -n "$want" ] || return 1
    want_pkg="$(cd "$SKILL_DIR" 2>/dev/null && pwd -P)" || return 1
    health="$(curl -s --max-time 2 "http://127.0.0.1:${port}/health" 2>/dev/null)" || return 1
    case "$health" in
        *"\"version\":\"${want}\""* | *"\"version\": \"${want}\""*) ;;
        *) return 1 ;;
    esac
    case "$health" in
        *"\"package\":\"${want_pkg}\""* | *"\"package\": \"${want_pkg}\""*) ;;
        *) return 1 ;;
    esac
    probe="$(curl -s -o /dev/null -w '%{http_code} %{content_type}' --max-time 2 "http://127.0.0.1:${port}/" 2>/dev/null)" || return 1
    case "$probe" in "200 text/html"*) return 0 ;; esac
    return 1
}

# C3: the Control Plane is the default UI. Prints a live loopback URL from
# ~/.loki/control/instance.json (pid alive, http loopback only, /health says
# service=loki-control), or starts `loki control serve` detached and waits for
# it to publish one. Returns 1 when the caller should fall back to the old
# dashboard (LOKI_CONTROL_DEFAULT=0, no bun, no python3, server did not start).
_loki_control_live_url() {
    local inst="${HOME}/.loki/control/instance.json" fields pid url health
    [ -f "$inst" ] || return 1
    fields=$(python3 - "$inst" 2>/dev/null <<'PY'
import json, sys, urllib.parse
d = json.load(open(sys.argv[1]))
pid, url = d.get("pid"), d.get("url")
if not isinstance(pid, int) or isinstance(pid, bool) or not isinstance(url, str):
    sys.exit(1)
u = urllib.parse.urlparse(url)
if u.scheme != "http" or u.hostname not in ("127.0.0.1", "localhost", "::1"):
    sys.exit(1)
if u.path not in ("", "/") or u.query or u.fragment or u.params or "@" in u.netloc or "?" in url or "#" in url:
    sys.exit(1)
print(pid, url.rstrip("/"))
PY
) || return 1
    pid="${fields%% *}"; url="${fields#* }"
    kill -0 "$pid" 2>/dev/null || return 1
    health=$(curl -g -s --max-time 2 "${url}/health" 2>/dev/null) || return 1
    case "$health" in *'"service"'*'"loki-control"'*) ;; *) return 1 ;; esac
    printf '%s\n' "$url"
}

_loki_control_ui() {
    [ "${LOKI_CONTROL_DEFAULT:-1}" = "0" ] && return 1
    local url="" open_it=1 arg i
    for arg in "$@"; do [ "$arg" = "--no-open" ] && open_it=0; done
    if [ "${LOKI_HEADLESS:-0}" = "1" ] || [ "${LOKI_NO_BROWSER:-0}" = "1" ]; then open_it=0; fi
    command -v python3 >/dev/null 2>&1 || return 1
    url=$(_loki_control_live_url) || url=""
    if [ -z "$url" ]; then
        if ! command -v bun >/dev/null 2>&1; then
            echo "loki: the Control Plane needs bun (https://bun.sh); using the classic dashboard." >&2
            return 1
        fi
        # LOKI_CONTROL=0 is the test off switch (supervisor, run-all-tests, local-ci);
        # it must never start a detached `control serve` that writes ~/.loki/control.
        [ "${LOKI_CONTROL:-}" = "0" ] && return 1
        local cli="$SKILL_DIR/loki-ts/dist/loki.js" cdir="${HOME}/.loki/control" spid
        [ -f "$cli" ] || cli="$SKILL_DIR/loki-ts/src/cli.ts"
        mkdir -p "$cdir" && chmod 700 "$cdir" 2>/dev/null || true
        LOKI_CONTROL=1 LOKI_NO_BROWSER=1 nohup bun --no-env-file --config=/dev/null "$cli" control serve >"$cdir/serve.log" 2>&1 </dev/null &
        spid=$!
        echo "$spid" > "$cdir/serve.pid" 2>/dev/null || true
        for i in $(seq 1 100); do
            url=$(_loki_control_live_url) && break
            url=""
            kill -0 "$spid" 2>/dev/null || break
            sleep 0.1
        done
        if [ -z "$url" ]; then
            echo "loki: the Control Plane did not start (see $cdir/serve.log); using the classic dashboard." >&2
            return 1
        fi
    fi
    if [ "$open_it" = "1" ]; then
        loki_open_url "$url" || echo "$url"
    else
        echo "$url"
    fi
    echo "Give it work:   loki start \"<brief>\""
    echo "Stop the UI:    loki control stop"
    return 0
}

cmd_ui() {
    _loki_control_ui "$@" && return 0
    # CP-LEGACY: callers that must never start the classic server (run.sh
    # auto-open) set LOKI_UI_NO_CLASSIC=1.
    [ "${LOKI_UI_NO_CLASSIC:-0}" = "1" ] && return 1
    local port="" pid=""
    if [ -f "$DASHBOARD_PID_FILE" ]; then
        pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            port=$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)
            # A registry entry whose live server is not this package's dashboard
            # (stale, foreign worktree, other version) is never reused.
            _loki_dashboard_port_is_ours "$port" || port=""
        fi
    fi
    if [ -z "$port" ]; then
        port=$(loki_ui_free_port "${LOKI_DASHBOARD_PORT:-$DASHBOARD_DEFAULT_PORT}")
        [ -n "$port" ] || { echo "loki: no free local port found" >&2; return 1; }
        # The WS-origin allowlist must name the chosen port on both loopback names.
        LOKI_PROJECT_DIR="$PWD" \
            LOKI_DASHBOARD_CORS="http://localhost:$port,http://127.0.0.1:$port" \
            cmd_dashboard_start --host 127.0.0.1 --port "$port" >&2 || return 1
    fi
    # The classic server is API-only; the UI is the Control Plane.
    local url="http://127.0.0.1:${port}"
    echo "loki: the classic dashboard at $url serves the API only (the browser UI moved to the Control Plane)." >&2
    echo "loki: start the Control Plane UI with: loki ui (needs bun)" >&2
    echo "$url"
}

# Tight newcomer landing for the bare `loki` invocation (no args).
# v7.28.x UX: the full 70-command table lives in `loki help`; a no-args run
# now shows only a calm ~12-line orientation so the "what do I do first"
# block is never buried under the command wall. `loki help`/`--help` are
# unchanged (still call show_help).
show_landing() {
    local version
    version=$(get_version)
    # v7.30.0 (item 5): strip color when stdout is not a TTY (piped/redirected)
    # in addition to the global NO_COLOR honoring above, so captured landing
    # output is clean. Local overrides only; the rest of the CLI is untouched.
    local BOLD="$BOLD" CYAN="$CYAN" NC="$NC"
    if [ ! -t 1 ]; then
        BOLD=''; CYAN=''; NC=''
    fi
    echo -e "${BOLD}Loki Mode v$version${NC} - the spec-driven builder that verifies its own work."
    echo ""
    echo -e "First time here? ${CYAN}loki doctor${NC} checks your setup (an AI provider CLI is required)."
    echo ""
    echo "Get started (in order):"
    echo -e "  ${CYAN}loki tour${NC}             See a real result now (no provider, no key, no spend)"
    echo -e "  ${CYAN}loki doctor${NC}           Check your setup is ready (a few seconds)"
    echo -e "  ${CYAN}loki quickstart${NC}       Guided first build from your idea (no PRD needed)"
    echo -e "  ${CYAN}loki start ./prd.md${NC}   Build from a spec (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
    echo "  quickstart = guided first build; quick = one small task (Loki 10 engine, no PR)."
    echo ""
    echo -e "Need help? ${CYAN}loki help${NC} lists every command."
    echo "Tip: preview cost and scope before building: loki plan <your-prd.md>"
}

# Detect argument type for unified `loki start` (v6.84.0)
# Returns one of: prd | issue | repo | empty | unknown
# Logic:
#   - empty arg -> "empty"
#   - file path ending in .md/.json/.txt AND exists on disk -> "prd"
#   - file path ending in .md/.json/.txt (doesn't exist) -> "prd" (let downstream handle)
#   - URL matching github.com/gitlab.com/atlassian.net/dev.azure.com/visualstudio.com issues -> "issue"
#   - PROJ-123 style Jira key -> "issue"
#   - owner/repo#123 -> "issue"
#   - #NUM or bare NUM -> "issue" (GitHub issue in current repo)
#   - Anything else that looks like a filesystem path -> "prd"
#   - Fallback -> "unknown" (caller treats as PRD path for back-compat)
detect_arg_type() {
    local arg="${1:-}"

    # Empty arg
    if [ -z "$arg" ]; then
        echo "empty"
        return 0
    fi

    # Sentry intake: an https Sentry issue URL, or an exported Sentry JSON file
    # on disk, is an issue (checked before the .json -> prd rule below).
    if [[ "$arg" =~ ^https://[^/]*sentry[^/]*/.*issues/[0-9]+ ]]; then
        echo "issue"
        return 0
    fi
    if [[ "$arg" == *.json ]] && [ -f "$arg" ] \
        && head -c 2000000 "$arg" 2>/dev/null | grep -Eq '"(shortId|culprit)"[[:space:]]*:|"exception"[[:space:]]*:|"type"[[:space:]]*:[[:space:]]*"exception"'; then
        echo "issue"
        return 0
    fi

    # Known PRD extensions - treat as PRD first (highest priority).
    # This handles edge case where a file is literally named "123.md" or
    # sits in a dir that matches another pattern.
    case "$arg" in
        *.md|*.json|*.txt|*.yaml|*.yml)
            echo "prd"
            return 0
            ;;
    esac

    # Issue URLs -- order matters: check explicit tracker hosts first
    if [[ "$arg" =~ github\.com/[^/]+/[^/]+/issues/[0-9]+ ]]; then
        echo "issue"
        return 0
    fi
    if [[ "$arg" =~ gitlab\.com/.+/-/issues/[0-9]+ ]] || [[ "$arg" =~ gitlab\..+/-/issues/[0-9]+ ]]; then
        echo "issue"
        return 0
    fi
    if [[ "$arg" =~ \.atlassian\.net/browse/[A-Z]+-[0-9]+ ]] || [[ "$arg" =~ jira\..+/browse/[A-Z]+-[0-9]+ ]]; then
        echo "issue"
        return 0
    fi
    if [[ "$arg" =~ dev\.azure\.com/.+/_workitems/edit/[0-9]+ ]] || [[ "$arg" =~ visualstudio\.com/.+/_workitems/edit/[0-9]+ ]]; then
        echo "issue"
        return 0
    fi

    # owner/repo#123
    if [[ "$arg" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$ ]]; then
        echo "issue"
        return 0
    fi

    # Jira project key: PROJ-123
    if [[ "$arg" =~ ^[A-Z][A-Z0-9]+-[0-9]+$ ]]; then
        echo "issue"
        return 0
    fi

    # #123 or bare number -- treat as GitHub issue in current repo,
    # BUT only if a file with that literal name does NOT exist on disk
    # (ambiguity resolution: files-on-disk win over issue-number heuristic).
    if [[ "$arg" =~ ^#?[0-9]+$ ]]; then
        if [ -d "$arg" ]; then
            # Directory on disk wins -- same rule as the -d branch below.
            echo "repo"
        elif [ -e "$arg" ]; then
            # File on disk wins -- treat as PRD
            echo "prd"
        else
            echo "issue"
        fi
        return 0
    fi

    # An existing DIRECTORY is a repo to improve, not a spec to build from.
    # Before this, a directory fell into the "prd" branch below and was handed
    # to run.sh as a PRD path; it has no .md-family extension, so the
    # not-found guard skipped it and the run silently proceeded as if no spec
    # had been given. `loki start <dir>` now means "work on that repo".
    # ponytail: a directory named `spec.md` still returns "prd" -- the
    # extension branch at the top of this function wins. Deliberate: an
    # explicit spec extension is the stronger signal. Move this block above
    # that branch if directories-with-spec-names ever matter.
    if [ -d "$arg" ]; then
        echo "repo"
        return 0
    fi

    # An existing file on disk is unambiguously a PRD, even if its name has
    # spaces -- a real path the user pointed at wins over the brief heuristic.
    if [ -e "$arg" ]; then
        echo "prd"
        return 0
    fi

    # R7 (zero-config first run): a one-line brief. An arg that contains
    # WHITESPACE and is not an existing file is a natural-language brief, NOT a
    # path -- a real PRD path argument essentially never contains spaces, whereas
    # a brief routinely contains slashes/punctuation ("add/list/done", "CI/CD",
    # "input/output"). This MUST come before the path-like (contains "/")
    # heuristic below, else a brief like "a todo app with add/list/done" is
    # misrouted to PRD mode and dies with "PRD file not found" (W1 E2E bug,
    # v7.89.1). The `--brief` flag remains the escape hatch for a single-word
    # brief with no whitespace.
    if [[ "$arg" == *[[:space:]]* ]]; then
        echo "brief"
        return 0
    fi

    # Path-like (contains / or starts with . or /) -> treat as PRD. Reached only
    # for a NO-WHITESPACE arg (a brief with spaces was already handled above), so
    # this is a genuine file path like ./prd.md or docs/spec.md.
    if [[ "$arg" == */* ]] || [[ "$arg" == .* ]] || [[ "$arg" == /* ]]; then
        echo "prd"
        return 0
    fi

    # Fallback: unknown. Caller treats as PRD for backward compat.
    echo "unknown"
}

# --- loki start detach-default handoff --------------------------------
# On an interactive TTY, `loki start` now detaches the build to the background
# by default after a rich, reversible handoff card. A non-interactive shell,
# --bg, --yes/-y, or CI takes the exact pre-existing foreground path (no card,
# byte-identical behavior). See tests/test-start-handoff.sh.

# Decide whether the interactive handoff should fire. Returns 0 (fire) only when
# stdout is a TTY, --bg was not requested, auto-confirm/-y is off, and we are not
# in CI / a non-interactive shell. Any other case returns 1 (take today's path).
# $1 = "1" if --bg is present in args, else "".
_loki_start_should_handoff() {
    local bg_requested="${1:-}"
    [ "$bg_requested" = "1" ] && return 1
    [ -t 1 ] || return 1
    [ -t 0 ] || return 1
    [ "${LOKI_AUTO_CONFIRM:-}" = "true" ] && return 1
    [ "${CI:-}" = "true" ] && return 1
    [ "${LOKI_NO_HANDOFF:-}" = "1" ] && return 1
    return 0
}

# Stare-worthy brand banner. Renders the real Autonomi logo (purple squircle
# + white "A" + teal dot) as a crafted truecolor block, next to a "LOKI" wordmark,
# straight to stderr. ZERO new tooling: pure ANSI escapes. Degrades by color depth
# ($COLORTERM truecolor -> 24-bit; else a clean 256/mono badge). Never required,
# always the default -- this is the moment the user stares at.
_loki_brand_banner() {
    local proj="$1"
    local nc=$'\033[0m' b=$'\033[1m' dim=$'\033[2m'
    # Truecolor when the terminal advertises it (nearly all modern terminals).
    if [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then
        local pB=$'\033[48;2;85;61;233m'    # squircle purple bg #553DE9
        local wF=$'\033[38;2;255;254;251m'  # white A
        local tF=$'\033[38;2;31;197;168m'   # teal dot
        local pF=$'\033[38;2;123;107;240m'  # light purple wordmark
        # 6-row squircle badge; each cell two chars. '#'=A stroke, 'o'=teal dot.
        local rows=(
            "......"
            "..##.."
            ".#..#."
            ".####."
            ".#.o#."
            ".#..#."
        )
        printf '\n' >&2
        local i=0 line r c ch
        for r in "${rows[@]}"; do
            line=""
            for ((c=0; c<${#r}; c++)); do
                ch="${r:$c:1}"
                case "$ch" in
                    '#') line+="${pB}${wF}\xE2\x96\x88\xE2\x96\x88${nc}" ;;
                    'o') line+="${pB}${tF}\xE2\x97\x8F ${nc}" ;;
                    *)   line+="${pB}  ${nc}" ;;
                esac
            done
            # Wordmark + tagline riding beside the badge on two of the rows.
            case $i in
                2) printf "   %b   %b%bLOKI%b\n" "$line" "$b" "$pF" "$nc" >&2 ;;
                3) printf "   %b   %bAutonomi cockpit%b\n" "$line" "$dim" "$nc" >&2 ;;
                *) printf "   %b\n" "$line" >&2 ;;
            esac
            i=$((i+1))
        done
        printf '\n' >&2
    else
        # 256-color / limited: a compact, still-branded badge (no truecolor blocks).
        local p=$'\033[38;5;99m' t=$'\033[38;5;43m'
        printf '\n   %s%s\xE2\x97\x86%s %sLOKI%s %s\xE2\x97\x8F%s %sAutonomi cockpit%s\n\n' \
            "$b" "$p" "$nc" "$b" "$nc" "$t" "$nc" "$dim" "$nc" >&2
    fi
}

# a 3-line "what Loki will do" preview from REAL signals only. Line 1 ties
# the detected tier to its phase count (simple=3, standard=6, complex=8 -- the
# same mapping run.sh uses; auto/unknown stays honest as "auto-detecting"). Line
# 2 names the spec, or admits "exploring the codebase" when there is none. Line 3
# is the fixed RARV-C closure promise. Never fabricates. Prints 3 lines to stdout.
# Args: $1=spec label  $2=tier
_loki_plan_preview() {
    local spec="$1" tier="$2"
    local phases shape
    case "$(printf '%s' "$tier" | tr '[:upper:]' '[:lower:]')" in
        simple)          phases="3 phases";  shape="a focused build" ;;
        standard)        phases="6 phases";  shape="a full SDLC pass" ;;
        complex)         phases="8 phases";  shape="a deep multi-phase build" ;;
        *)               phases="phase count auto-detecting"; shape="scope from your spec" ;;
    esac
    printf '   plan       %s -- %s\n' "$phases" "$shape"
    case "$spec" in
        ""|*"no PRD"*|*"codebase"*)
            printf '   from       exploring the codebase (no spec file)\n' ;;
        *)
            printf '   from       %s\n' "$spec" ;;
    esac
    printf '   loop       Reason - Act - Reflect - Verify - Close, until verified\n'
}

# Print the rich handoff card and resolve the user's choice. Echoes exactly one
# decision word on stdout:
#   launch-bg           detach only
#   launch-bg-dashboard detach + open the dashboard in a browser
#   launch-bg-watch     detach + print the `loki cockpit` hint
#   cancel              do not launch (user chose Stop)
# All non-cancel decisions detach the build (arm --bg). The choice is remembered
# per project under .loki/config/start-view.json; a remembered choice skips the
# prompt and prints only a one-line summary.
# Args: $1=spec label  $2=tier  $3=provider
_loki_start_handoff() {
    local spec="$1" tier="$2" prov="$3"
    local port="${LOKI_DASHBOARD_PORT:-57374}"
    local dash_url="http://127.0.0.1:${port}"
    local budget="${LOKI_BUDGET_LIMIT:-none}"
    local cfg_dir="${LOKI_DIR:-.loki}/config"
    local cfg_file="$cfg_dir/start-view.json"
    local pwd_path; pwd_path="$(pwd)"
    local proj; proj="$(basename "$pwd_path")"

    # Remembered choice: skip the prompt, print one-line summary, reuse decision.
    local saved=""
    if [ -f "$cfg_file" ]; then
        saved="$(sed -n 's/.*"view"[[:space:]]*:[[:space:]]*"\([a-z-]*\)".*/\1/p' "$cfg_file" 2>/dev/null | head -1)"
    fi
    if [ -n "$saved" ]; then
        case "$saved" in
            both|dashboard|watch|detach)
                echo -e "${DIM}Detaching to background (remembered view: ${saved}; change with 'loki cockpit' or delete ${cfg_file}).${NC}" >&2
                case "$saved" in
                    dashboard) echo "launch-bg-dashboard" ;;
                    watch)     echo "launch-bg-watch" ;;
                    *)         echo "launch-bg" ;;
                esac
                return 0
                ;;
        esac
    fi

    # Stare-worthy brand banner then the rich handoff card. Both to stderr so
    # stdout carries only the decision word.
    _loki_brand_banner "$proj"
    {
        echo -e "   ${BOLD}Building ${proj}${NC}  \033[38;2;31;197;168m\xE2\x97\x8F${NC} ${DIM}live${NC}"
        echo ""
        echo -e "   ${DIM}spec${NC}       ${spec}"
        echo -e "   ${DIM}path${NC}       ${pwd_path}"
        echo -e "   ${DIM}tier${NC}       ${tier}"
        echo -e "   ${DIM}provider${NC}   ${prov}"
        echo -e "   ${DIM}iteration${NC}  starting"
        echo -e "   ${DIM}budget${NC}     \$0 / ${budget} (breaker: on)"
        echo -e "   ${DIM}log${NC}        ${LOKI_DIR:-.loki}/logs/background-*.log"
        echo -e "   ${DIM}dashboard${NC}  ${dash_url}"
        echo ""
        # E4 plan preview: 3 lines of real intent before backgrounding.
        _loki_plan_preview "$spec" "$tier"
        echo ""
        echo -e "   ${BOLD}Both${NC}[default]  ${BOLD}D${NC}ashboard  ${BOLD}W${NC}atch  De${BOLD}t${NC}ach  ${BOLD}S${NC}top"
        echo -e "   ${DIM}opening your cockpit + dashboard in 8s -- press a key to choose${NC}"
        # First-run delight: only when this project has never chosen a view, teach
        # the cockpit once. Subsequent runs (config file exists) stay lean.
        if [ ! -f "$cfg_file" ]; then
            echo -e "   ${DIM}first build here -- 'loki cockpit' reopens this live view any time${NC}"
        fi
    } >&2

    local reply=""
    read -t 8 -r reply 2>/dev/null || reply=""

    local decision="launch-bg" view="both"
    case "$(printf '%s' "$reply" | tr '[:upper:]' '[:lower:]')" in
        ""|b|both)          decision="launch-bg"; view="both" ;;
        d|dash|dashboard)   decision="launch-bg-dashboard"; view="dashboard" ;;
        w|watch)            decision="launch-bg-watch"; view="watch" ;;
        t|detach)           decision="launch-bg"; view="detach" ;;
        s|stop|cancel|q)    decision="cancel"; view="" ;;
        *)                  decision="launch-bg"; view="both" ;;
    esac

    if [ "$decision" = "cancel" ]; then
        echo -e "${YELLOW}run cancelled${NC}" >&2
        echo "cancel"
        return 0
    fi

    # Remember the choice for next time (best-effort; never blocks the launch).
    mkdir -p "$cfg_dir" 2>/dev/null && \
        printf '{"view":"%s"}\n' "$view" > "$cfg_file" 2>/dev/null || true

    if [ "$view" = "both" ]; then
        echo -e "${DIM}Open the dashboard at ${dash_url} or run 'loki cockpit' to watch.${NC}" >&2
    elif [ "$view" = "watch" ]; then
        echo -e "${DIM}Run 'loki cockpit' to watch the build.${NC}" >&2
    fi

    echo "$decision"
    return 0
}

# Start Loki Mode
# The ONE allowlist of cmd_start flags. _loki_start_flag_kind sets
# _LOKI_START_FLAG_KIND to value | optional | bool, or returns 1 for an unknown
# flag. The --workspace brief scan and the --resume scan both read it, so a value
# is never mistaken for a brief or run id and an unknown flag exits 64 like the
# parser's own catch-all. tests/test-cli-allow-haiku-flag.sh checks it against the
# cmd_start case arms, so the parser and this list cannot drift apart. --model and
# the other Bun/engine10 flags are handled by bin/loki before cmd_start runs, so
# they are unknown to cmd_start's pre-scans by design.
_loki_start_flag_kind() {
    _LOKI_START_FLAG_KIND=""
    case "${1%%=*}" in
        --provider|--brief|--budget|--cost|--prd|--issue|--isolation|--aider-model|--aider-flags|--cline-model|--compliance|--max-duration|--log-level|--bmad-project|--openspec|--mirofish-rounds|--mirofish-timeout|--output|--config|--vars|--env-file|--remote|--attempts|--mirofish-docker) _LOKI_START_FLAG_KIND=value ;;
        --mirofish) _LOKI_START_FLAG_KIND=optional ;;
        --parallel|--allow-haiku|--haiku|--regen-prd|--regenerate-prd|--regen|--fresh-prd|--bg|--background|--simple|--complex|--github|--no-dashboard|--api|--sandbox|--skip-memory|--yes|-y|--quiet|--mirofish-bg|--no-mirofish|--no-plan|--fast|--repo-fast|--full|--repo-full|--dry-run|--no-start|--worktree|-w|--pr|--prepare-pr|--ship|--detach|-d|--help|-h|--resume|--) _LOKI_START_FLAG_KIND=bool ;;
        *) return 1 ;;
    esac
    # A "--flag=value" token carries its own value, so it never consumes the next one.
    if [[ "$1" == --*=* ]]; then _LOKI_START_FLAG_KIND=bool; fi
    return 0
}

# Optional-value cmd_start flags (--mirofish [URL]). The one decision on whether
# the NEXT token is the flag's value: non-empty and not another --flag. The
# cmd_start --mirofish arm and both scans use it, so they cannot disagree.
_loki_start_optval_consumes() { [[ -n "${1:-}" && "${1:-}" != --* ]]; }
# Same message and exit code as the cmd_start parser's catch-all arm.
_loki_start_unknown_flag() { echo -e "${RED}Unknown option: $1${NC}"; exit 64; }

# Exit-code mapping for the engine10 resume REPORTER, used only by cmd_resume.
# 0 and 4 (phase reported) -> 0, 5 (publish failed) -> 1. `loki start --resume`
# runs the stage resume (engine10 --resume) and returns its code unchanged.
# Sets _LOKI_E10_RC and returns 0 when handled; any other code returns 1 with
# _LOKI_E10_RC set to the raw code (cmd_resume falls through).
_loki_e10_resume_map() {
    case "$1" in
        0 | 4) _LOKI_E10_RC=0 ;;
        5) _LOKI_E10_RC=1 ;;
        *) _LOKI_E10_RC="$1"; return 1 ;;
    esac
    return 0
}

# TEN-COST-01 lean levers (the bash twin of COST_PROFILES.lean in loki-ts/src/util/cost_profile.ts).
# Fills only unset variables, so explicit env wins. Shared by --cost lean and --haiku.
loki_lean_levers() {
    : "${LOKI_IMPLEMENT_HAIKU_FIRST:=1}" "${LOKI_E10_TRIM:=1}" "${LOKI_E10_SMALL_NO_RUNNER:=1}" "${LOKI_E10_SMALL_MAX_CHARS:=1200}"
    export LOKI_IMPLEMENT_HAIKU_FIRST LOKI_E10_TRIM LOKI_E10_SMALL_NO_RUNNER LOKI_E10_SMALL_MAX_CHARS
}

# --haiku opt-in: the lean levers plus LOKI_COST_FIRST=1. Does not change the
# default model. bin/loki strips the flag and sets the same levers before
# engine10; this covers a direct bash start and LOKI_CONFIG_DUMP.
loki_apply_haiku_optin() {
    local _a
    for _a in "$@"; do
        [ "$_a" = "--haiku" ] || continue
        loki_lean_levers
        : "${LOKI_COST_FIRST:=1}"
        export LOKI_COST_FIRST
        return 0
    done
}

# Same fail-to-pass default as _loki_f2p_default in bin/loki. Direct bash
# starts never see the shim. Explicit 0 or 1 wins. --help does not set it.
loki_f2p_default() {
    case "${1:-}" in
        start|run|quick) ;;
        *) return 0 ;;
    esac
    local _a
    for _a in "$@"; do
        case "$_a" in --help|-h) return 0 ;; esac
    done
    if [ -z "${LOKI_MUTATION_STRICT:-}" ]; then
        export LOKI_MUTATION_STRICT=1
        export LOKI_F2P_DEFAULTED=1
    fi
    if [ -z "${LOKI_E10_TRIM:-}" ]; then
        export LOKI_E10_TRIM=1
        export LOKI_TRIM_DEFAULTED=1
    fi
}
# Names only the defaults this run actually turned on (an explicit 0 or 1 is the user's, not a default).
loki_f2p_banner() {
    local f2p="${LOKI_F2P_DEFAULTED:-}" trim="${LOKI_TRIM_DEFAULTED:-}"
    if [ "$f2p" = "1" ] && [ "$trim" = "1" ]; then
        echo "Factory: a behavior change is verified only when a Wall test fails on the old code, and tool logs are trimmed before the next turn. Opt out: LOKI_MUTATION_STRICT=0, LOKI_E10_TRIM=0." >&2
    elif [ "$f2p" = "1" ]; then
        echo "Factory: a behavior change is verified only when a Wall test fails on the old code. Opt out: LOKI_MUTATION_STRICT=0." >&2
    elif [ "$trim" = "1" ]; then
        echo "Factory: tool logs are trimmed before the next turn. Opt out: LOKI_E10_TRIM=0." >&2
    fi
}

# $1 (optional): the repo the run targets, so `loki factory ./other-repo` reports that repo's policy.
loki_print_autonomy() {
    local pol="" f pre="" data_dir="${LOKI_DATA_DIR:-${HOME}/.loki}"
    if [ -n "${1:-}" ] && [ "$1" != "." ] && [ -d "$1" ]; then pre="${1%/}/"; fi
    for f in "${pre}.loki/policies.json" "${pre}.loki/policies.yaml" "${data_dir}/policies.json" "${data_dir}/policies.yaml"; do
        if [ -f "$f" ]; then pol="$f"; break; fi
    done
    if [ -n "$pol" ]; then
        echo "Autonomy: policy file $pol (propose, execute, operate; the file decides PR allow, draft or deny)." >&2
    else
        echo "Autonomy: execute (no policy file). Levels: propose, execute, operate." >&2
    fi
}

cmd_start() {
    # First-run welcome (once): open the branded welcome page on the very
    # first start. Writes ~/.loki/.welcomed and never repeats; non-blocking,
    # and never auto-opens a browser in CI or non-interactive shells.
    cmd_welcome_maybe_firstrun 2>/dev/null || true

    # Tell the user when their install is stale, on the command they ACTUALLY
    # run. The hint existed but was wired only into `doctor` and `version` --
    # neither of which anyone runs before a build -- so a stale install stayed
    # invisible for as long as it took someone to go looking.
    #
    # That is not hypothetical. This machine was 4 releases behind (8.8.0 vs
    # 8.41.0) while running builds, and that specific gap mattered: v8.38.0
    # fixed four quality-gate detectors that had NEVER shipped, so on 8.8.0
    # mutation-integrity fail-closed on every iteration and first-pass
    # completion was impossible no matter how good the output was. The user had
    # no way to know from the tool.
    #
    # Cheap and fail-silent by construction: the result is cached for 24h, the
    # registry call has a 1.5s hard timeout, and any failure prints nothing
    # rather than guessing (see maybe_print_update_hint).
    # NOT `2>/dev/null`: the hint is PRINTED ON STDERR, so redirecting stderr
    # discards the only thing this call exists to produce. A first draft did
    # exactly that and the feature was silently dead. The function is already
    # fail-silent internally (opt-out, CI, non-TTY, timeout, unparseable
    # version all return 0 without output), so no guard is needed here.
    maybe_print_update_hint || true

    # PATH slice: --workspace NAME runs a multi-repo workspace through the one
    # workspace runner (no new logic here), and --deploy [--health-url U]
    # [--auto-rollback] chains the gated `loki deploy --execute` after a start
    # that ended 0. cmd_deploy owns every gate: no VERIFIED HEAD-anchored
    # receipt means a refusal, never a deploy.
    local _ps_ws="" _ps_deploy=0 _ps_health="" _ps_hmatch="" _ps_rb=0 _ps_rest=() _ps_brief=() _ps_a
    local _ps_dest="" _ps_canary=0 _ps_envf="" _ps_domain="" _ps_gh=0 _ps_dep_in=0
    # --env-file and --github already mean config alias and issue sync for a plain
    # start; they are deploy flags only when --deploy is on the line.
    for _ps_a in "$@"; do [ "$_ps_a" = "--deploy" ] && _ps_dep_in=1; done
    while [ $# -gt 0 ]; do
        case "$1" in
            --destination)
                [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--destination requires preview, staging or production${NC}" >&2; exit 2; }
                _ps_dest="$2"; shift 2 ;;
            --destination=*) _ps_dest="${1#*=}"; shift ;;
            --domain)
                [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--domain requires a hostname${NC}" >&2; exit 2; }
                _ps_domain="$2"; shift 2 ;;
            --domain=*) _ps_domain="${1#*=}"; shift ;;
            --env-file)
                if [ "$_ps_dep_in" != 1 ]; then _ps_rest+=("$1"); shift; continue; fi
                [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--env-file requires a file path${NC}" >&2; exit 2; }
                _ps_envf="$2"; shift 2 ;;
            --env-file=*)
                if [ "$_ps_dep_in" = 1 ]; then _ps_envf="${1#*=}"; else _ps_rest+=("$1"); fi
                shift ;;
            --canary) _ps_canary=1; shift ;;
            --github)
                if [ "$_ps_dep_in" = 1 ]; then _ps_gh=1; else _ps_rest+=("$1"); fi
                shift ;;
            --workspace)
                [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--workspace requires a workspace name${NC}" >&2; exit 2; }
                _ps_ws="$2"; shift 2 ;;
            --workspace=*) _ps_ws="${1#*=}"; shift ;;
            --deploy) _ps_deploy=1; shift ;;
            --health-url)
                [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-url requires a URL${NC}" >&2; exit 2; }
                _ps_health="$2"; shift 2 ;;
            --health-url=*) _ps_health="${1#*=}"; shift ;;
            --health-match)
                [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-match requires a string${NC}" >&2; exit 2; }
                _ps_hmatch="$2"; shift 2 ;;
            --health-match=*) _ps_hmatch="${1#*=}"; shift ;;
            --auto-rollback) _ps_rb=1; shift ;;
            *) _ps_rest+=("$1"); shift ;;
        esac
    done
    if { [ -n "$_ps_health" ] || [ -n "$_ps_hmatch" ] || [ "$_ps_rb" = 1 ] || [ -n "$_ps_dest" ] || [ "$_ps_canary" = 1 ] || [ -n "$_ps_domain" ]; } && [ "$_ps_deploy" != 1 ]; then
        echo -e "${RED}--health-url, --health-match, --auto-rollback, --destination, --canary and --domain apply only with --deploy${NC}" >&2
        exit 2
    fi
    # Same checks and text as cmd_deploy so the bash route fails before cmd_start.
    if [ "$_ps_deploy" = 1 ]; then
        if [ -n "$_ps_dest" ]; then
            case "$_ps_dest" in
                preview|staging|production) ;;
                *) echo -e "${RED}loki deploy --destination must be one of: preview, staging, production${NC}" >&2; exit 2 ;;
            esac
        fi
        if [ "$_ps_rb" = 1 ] && [ -z "$_ps_health" ]; then
            echo -e "${RED}loki deploy --auto-rollback requires --health-url (no health signal, no rollback)${NC}" >&2
            exit 2
        fi
        if [ -n "$_ps_hmatch" ] && [ -z "$_ps_health" ]; then
            echo -e "${RED}loki deploy --health-match requires --health-url${NC}" >&2
            exit 2
        fi
        if [ "$_ps_canary" = 1 ] && [ -n "$_ps_dest" ] && [ "$_ps_dest" != production ]; then
            echo -e "${RED}loki deploy --canary only applies to --destination production${NC}" >&2
            exit 2
        fi
    fi
    if [ -n "$_ps_ws" ]; then
        if [ "$_ps_deploy" = 1 ]; then
            echo -e "${RED}--workspace and --deploy cannot be combined: deploy each repo with loki deploy${NC}" >&2
            exit 2
        fi
        local _ps_skip=0
        for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
            if [ "$_ps_skip" = 1 ]; then _ps_skip=0; continue; fi
            if [ "$_ps_skip" = 3 ]; then _ps_skip=0; if _loki_start_optval_consumes "$_ps_a"; then continue; fi; fi
            if [ "$_ps_skip" = 2 ]; then _ps_skip=0; _ps_brief+=("$_ps_a"); continue; fi
            case "$_ps_a" in
                --brief=*) _ps_brief+=("${_ps_a#*=}") ;;
                --brief) _ps_skip=2 ;;
                # bin/loki turns --model into --session-model and forwards --max-cost on
                # the legacy route; a workspace start ignores both. Skip them and their value.
                --session-model|--max-cost) _ps_skip=1 ;;
                --session-model=*|--max-cost=*) ;;
                # Every value-taking cmd_start flag: its value is never brief text.
                -*) _loki_start_flag_kind "$_ps_a" || _loki_start_unknown_flag "$_ps_a"
                    if [ "$_LOKI_START_FLAG_KIND" = value ]; then _ps_skip=1; elif [ "$_LOKI_START_FLAG_KIND" = optional ]; then _ps_skip=3; fi ;;
                *) _ps_brief+=("$_ps_a") ;;
            esac
        done
        if [ "${#_ps_brief[@]}" -eq 0 ]; then
            echo -e "${RED}loki start --workspace $_ps_ws needs a brief or an issue ref${NC}" >&2
            exit 2
        fi
        cmd_workspace run "$_ps_ws" "${_ps_brief[*]}"
        return $?
    fi
    if [ "$_ps_deploy" = 1 ]; then
        local _ps_rc=0
        ( cmd_start ${_ps_rest[@]+"${_ps_rest[@]}"} ) || _ps_rc=$?
        if [ "$_ps_rc" -ne 0 ]; then
            echo -e "${RED}start exited $_ps_rc: not deploying${NC}" >&2
            return "$_ps_rc"
        fi
        local _ps_dargs=(--execute)
        [ -n "$_ps_health" ] && _ps_dargs+=(--health-url "$_ps_health")
        [ -n "$_ps_hmatch" ] && _ps_dargs+=(--health-match "$_ps_hmatch")
        [ "$_ps_rb" = 1 ] && _ps_dargs+=(--auto-rollback)
        [ -n "$_ps_dest" ] && _ps_dargs+=(--destination "$_ps_dest")
        [ "$_ps_canary" = 1 ] && _ps_dargs+=(--canary)
        [ -n "$_ps_envf" ] && _ps_dargs+=(--env-file "$_ps_envf")
        [ -n "$_ps_domain" ] && _ps_dargs+=(--domain "$_ps_domain")
        local _ps_drc=0
        cmd_deploy "${_ps_dargs[@]}" || _ps_drc=$?
        if [ "$_ps_drc" -ne 0 ]; then return "$_ps_drc"; fi
        if [ "$_ps_gh" = 1 ]; then
            ( cmd_github publish ) || _ps_drc=$?
        fi
        return "$_ps_drc"
    fi
    # P12-07b, #352: a bare `loki start --resume [run-id]` goes to the engine10 stage
    # resume that `loki "<task>" --resume` uses (util/run_outputs.ts resumeFlag), never into a
    # new build and never to the reporter (resumeMain). The exit code is returned unchanged:
    # 3 nothing to resume, 2 refused, 4 blocked, 5 stalled; only `loki resume` maps codes.
    local _ps_resume=0 _ps_resume_args=() _ps_rskip=0 _ps_unknown=""
    for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
        if [ "$_ps_rskip" = 1 ]; then _ps_rskip=0; continue; fi
        if [ "$_ps_rskip" = 3 ]; then _ps_rskip=0; if _loki_start_optval_consumes "$_ps_a"; then continue; fi; fi
        case "$_ps_a" in
            --resume) _ps_resume=1 ;;
            -*) if ! _loki_start_flag_kind "$_ps_a"; then
                    [ -n "$_ps_unknown" ] || _ps_unknown="$_ps_a"
                elif [ "$_LOKI_START_FLAG_KIND" = value ]; then _ps_rskip=1
                elif [ "$_LOKI_START_FLAG_KIND" = optional ]; then _ps_rskip=3
                fi ;;
            *) _ps_resume_args+=("$_ps_a") ;;
        esac
    done
    # Without --resume the cmd_start parser reports the same error itself.
    if [ "$_ps_resume" = 1 ] && [ -n "$_ps_unknown" ]; then _loki_start_unknown_flag "$_ps_unknown"; fi
    if [ "$_ps_resume" = 1 ]; then
        local _ps_rrc=0
        for _ps_a in ${_ps_resume_args[@]+"${_ps_resume_args[@]}"}; do
            case "$_ps_a" in
                e10-[A-Za-z0-9-]*) ;;
                *)
                    echo -e "${RED}--resume resumes a sealed run; it takes an optional run id, not a task. Run \`loki start <task>\` for a new run.${NC}" >&2
                    return 2 ;;
            esac
        done
        ( _loki_bun_delegate engine10 --resume ${_ps_resume_args[@]+"${_ps_resume_args[@]}"} ) || _ps_rrc=$?
        return "$_ps_rrc"
    fi
    set -- ${_ps_rest[@]+"${_ps_rest[@]}"}

    local args=()
    local prd_file=""
    local provider=""
    local brief_text=""           # R7: explicit one-line brief (--brief "...")
    local bmad_project_path=""
    local openspec_change_path=""
    local mirofish_url=""
    local mirofish_docker_image=""
    local mirofish_bg=false
    local mirofish_disabled=false
    local no_plan=false  # v6.81.1: --no-plan opts out of auto-plan display
    local repo_depth="full"          # repository targets default to full verification
    local repo_depth_explicit=""     # "" | "fast" | "full" -- what the operator asked for
    local remote_url="${LOKI_REMOTE_URL:-}"   # --remote: submit to a deployed cluster

    # v6.84.0: unified entry point -- explicit mode overrides + issue-mode args
    local explicit_mode=""            # "prd" | "issue" | "" (auto-detect)
    local positional_arg=""           # raw positional arg for mode detection
    local issue_ref_explicit=""       # --issue value (when explicit_mode=issue)
    local issue_mode_args=()          # captured issue-mode flags (--dry-run, --pr, etc.)
    local issue_output_file=""
    local issue_use_worktree=false
    local issue_create_pr=false
    local issue_auto_merge=false
    local issue_run_detached=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki start${NC} - Unified entry point for Loki Mode"
                echo ""
                echo "Usage: loki start [REPO|PRD|ISSUE-REF] [options]"
                echo ""
                echo "Modes (auto-detected from input):"
                echo "  REPO mode   - existing directory -> one bounded verified improvement"
                echo "  PRD mode    - path ending in .md/.json/.txt/.yaml -> build from PRD"
                echo "  ISSUE mode  - GitHub/GitLab/Jira/Azure DevOps URL, owner/repo#N,"
                echo "                PROJ-123, #123, or bare number -> generate PRD from issue"
                echo "  BRIEF mode  - a quoted one-line description (with spaces) -> fast"
                echo "                zero-config first run; visible artifact + proof quickly"
                echo "  no input    - analyze current directory, auto-generate PRD (outside a git repo: the offline sample receipt)"
                echo ""
                echo "Arguments:"
                echo "  REPO                  Existing project directory"
                echo "  PRD                   Path to PRD file (.md/.json/.txt/.yaml)"
                echo "  ISSUE-REF             Issue URL, number, or tracker key (see above)"
                echo ""
                echo "Explicit mode flags (override auto-detection):"
                echo "  --prd FILE            Force PRD mode with FILE"
                echo "  --issue URL|NUM       Force issue mode with URL or number"
                echo "  --brief \"TEXT\"        Force zero-config brief mode (fast first run)"
                echo ""
                echo "Options:"
                echo "  --remote URL          Submit to a deployed Loki (POST /jobs) instead of"
                echo "                        building locally; polls until the job finishes."
                echo "                        Token from LOKI_REMOTE_TOKEN / LOKI_REMOTE_TOKEN_FILE"
                echo "                        (never an argument). Env: LOKI_REMOTE_URL."
                echo "  --provider NAME       AI provider: claude (default), codex, cline, aider, opencode"
                echo "  (model route: set LOKI_MODEL_ROUTE, for example ollama/qwen3-coder; no --model flag)"
                echo "  --allow-haiku        Enable Haiku model for the fast tier (default: disabled)"
                echo "  --simple              Force simple complexity tier (3 phases)"
                echo "  --complex             Force complex complexity tier (8 phases)"
                echo "  --no-dashboard        Disable web dashboard"
                echo "  --api                 Start dashboard API server alongside the build"
                echo "  --isolation LEVEL     Per-run isolation: none (default) | worktree | docker."
                echo "                        Unified dial."
                echo "  --attempts N          Run N (1-5, default 1) independent attempts in separate git worktrees;"
                echo "                        the most executed passing checks wins, losers are recorded on the receipt"
                echo "  --skip-memory         Skip loading memory context at startup"
                echo "  --spec FILE           Run against an edited .loki/specs/<slug>.md (from 'loki plan <task> --spec')"
                echo "  --spec-first          Write .loki/specs/<slug>.md for you to edit, then stop"
                echo "  --fresh-prd           Regenerate the PRD from the codebase on a no-PRD run"
                echo "                        (ignores the reusable generated PRD; aliases: --regen-prd,"
                echo "                        --regenerate-prd, --regen, or LOKI_PRD_REGEN=1)"
                echo "  --compliance PRESET   Enable compliance mode (default|healthcare|fintech|government)"
                echo "  --budget USD          Per-run cost cap (default $(loki_budget_default_usd); auto-pause when exceeded)"
                echo "  --cost PROFILE        Opt-in cost profile: lean (LOKI_IMPLEMENT_HAIKU_FIRST=1, LOKI_E10_TRIM=1, LOKI_E10_SMALL_NO_RUNNER=1, LOKI_E10_SMALL_MAX_CHARS=1200 unless already set) or off"
                echo "  --haiku                Opt in to haiku for the first implement. Same levers as --cost lean, plus LOKI_COST_FIRST=1. A failed check climbs back to the session model. Default is the provider model"
                echo "  Proof                 Default LOKI_MUTATION_STRICT=1 and LOKI_E10_TRIM=1. A behavior change is VERIFIED only when a Wall test fails on the old code. Tool logs are trimmed before the next turn; a command's tail is kept. Opt out: LOKI_MUTATION_STRICT=0, LOKI_E10_TRIM=0"
                echo "  --bmad-project PATH   Use BMAD Method project artifacts as input"
                echo "  --openspec PATH       Use OpenSpec change directory as input"
                echo "  --mirofish [URL]      Enable MiroFish market validation (default: localhost:5001)"
                echo "  --mirofish-docker IMG Auto-start MiroFish from Docker image"
                echo "  --mirofish-rounds N   Simulation rounds (default: 100)"
                echo "  --mirofish-timeout S  Max pipeline wait in seconds (default: 3600)"
                echo "  --mirofish-bg         Run MiroFish pipeline in background"
                echo "  --no-mirofish         Disable MiroFish even if env var is set"
                echo "  --no-plan             Skip auto-shown PRD analysis at startup"
                echo "  --fast                REPO mode: capped lightweight pass, gates reduced"
                echo "                        (no council verdict; alias --repo-fast)"
                echo "  --full                REPO mode: explicit full verification, the default"
                echo "                        (wins over --fast; alias --repo-full)"
                echo "  --brief \"TEXT\"        Zero-config fast first run from a one-line brief"
                echo "  --yes, -y             Skip confirmation prompts (auto-confirm)"
                echo "  --quiet               Suppress [INFO]/[STEP]; warnings and errors still print"
                echo "  --log-level LEVEL     debug | info | warn | error (default: info)"
                echo "  --max-duration DUR    Wall-clock cap: seconds, or 90m / 2h (default: none)"
                echo "  --deploy              After a start that exits 0, run the gated loki deploy --execute. Deploy flags:"
                echo "                        --destination preview|staging|production, --canary, --domain HOST,"
                echo "                        --env-file FILE (the deploy env file, not config, when --deploy is set),"
                echo "                        --health-url URL, --health-match STR, --auto-rollback,"
                echo "                        and, with --deploy, the flag --github runs loki github publish after a good deploy"
                echo ""
                echo "Issue-mode options (used when input is an ISSUE-REF or a --brief):"
                echo "  --dry-run             Validate flags, run the preflight and print the provider, model and cost cap without starting (issue mode: preview the PRD)"
                echo "  --no-start            Generate PRD but don't start execution"
                echo "  --output FILE         Save generated PRD to custom path"
                echo "  --worktree, -w        Git worktree isolation (separate branch)"
                echo "  --pr                  Worktree + auto-create PR"
                echo "  --ship                Worktree + PR + auto-merge"
                echo "  --detach, -d          Run in background (implies --worktree)"
                echo "  --resume [RUN_ID]     Resume an interrupted run at the stage it stopped in"
                echo "                        Exit 4 means the run cannot be resumed OR the resumed run ended BLOCKED; read the printed line to tell them apart."
                echo ""
                echo "Preview without spending (any input mode):"
                echo "  loki plan <PRD>       Analyze a spec WITHOUT executing: complexity,"
                echo "                        estimated iterations, tokens, and cost. Add --json"
                echo "                        for machine-readable output. This is the dry run"
                echo "                        for PRD and brief mode; --dry-run above is"
                echo "                        issue-mode only."
                echo "  LOKI_CONFIG_DUMP=1 loki start"
                echo "                        Print the resolved LOKI_* configuration and exit"
                echo "                        without starting a run or spending anything."
                echo ""
                echo "Environment Variables:"
                echo "  LOKI_PRD_FILE         Path to PRD file (alternative to positional arg)"
                echo "  LOKI_AUTO_CONFIRM     Set to 'true'/'false' to control prompts (takes precedence over CI)"
                echo "  CI                    Fallback: auto-confirms when 'true' and LOKI_AUTO_CONFIRM is unset"
                echo "  LOKI_MAX_ITERATIONS   Max iteration count"
                echo "  LOKI_BUDGET_LIMIT     Cost cap in USD (default $(loki_budget_default_usd); empty means unlimited)"
                echo ""
                echo "Examples:"
                echo "  loki start                              # Interactive, analyze current dir (no git repo: offline sample)"
                echo "  loki start ./my-project                 # Improve an existing project"
                echo "  loki start \"build a todo app\"           # BRIEF mode (zero-config fast run)"
                echo "  loki start --brief \"snake\"              # BRIEF mode (single-word escape hatch)"
                echo "  loki start ./prd.md                     # PRD mode"
                echo "  loki start https://github.com/o/r/issues/42  # ISSUE mode (GitHub)"
                echo "  loki start 123                          # ISSUE mode (current repo GitHub issue)"
                echo "  loki start PROJ-456                     # ISSUE mode (Jira)"
                echo "  loki start owner/repo#789               # ISSUE mode (GitHub specific repo)"
                echo "  loki start --issue 123 --ship           # Explicit issue, full automation"
                echo "  loki start --provider codex             # Use OpenAI Codex CLI"
                echo "  loki start --bmad-project ./my-project  # Start from BMAD artifacts"
                echo "  loki start --openspec ./openspec/changes/my-feature  # From OpenSpec change"
                echo "  loki start --yes                        # Skip confirmation prompt"
                echo "  LOKI_PRD_FILE=./prd.md loki start       # PRD via env var"
                echo ""
                # Exit codes, stated where a CI author actually looks. The full
                # contract lived only in docs/exit-codes.md, so a script author
                # read this help, saw nothing, and built the coarse gate -- the
                # tiered contract might as well not have shipped.
                echo "Exit codes:"
                echo "  0                      The run completed (verified), or a human stopped it"
                echo "  Default engine run outcomes: 1 failed, 3 budget stop, 4 blocked,"
                echo "                         5 stalled, 6 partial (done, not fully proven), 130 cancelled"
                echo ""
                echo "  With LOKI_DURABLE_STATE=1 the platform contract applies, which"
                echo "  distinguishes 'retrying cannot help' from 'the process died':"
                echo "    0                    Completed, or a human stopped it -> do not retry"
                echo "    20                   Deterministic terminal failure (gate failed, max"
                echo "                         iterations or retries, budget or wall-clock cap,"
                echo "                         policy block, contradictory spec) -> do not retry;"
                echo "                         the same inputs fail the same way"
                echo "    other nonzero        Crash (SIGKILL, eviction, node loss) -> retry;"
                echo "                         the run resumes from the durable volume"
                echo "  Both the bash and Bun runners implement this identically."
                echo "  Full table, including loki verify and loki ci: docs/exit-codes.md"
                echo ""
                echo "Note: 'loki run' is a deprecated alias for 'loki start' with an issue ref."
                exit 0
                ;;
            --provider)
                if [[ -n "${2:-}" ]]; then
                    if [[ "$2" == --* ]]; then
                        echo -e "${RED}Missing value for --provider flag${NC}"
                        exit 1
                    fi
                    provider="$2"
                    args+=("--provider" "$provider")
                    shift 2
                else
                    echo -e "${RED}--provider requires a value (claude, codex, cline, aider, opencode)${NC}"
                    exit 1
                fi
                ;;
            --provider=*)
                provider="${1#*=}"
                args+=("--provider" "$provider")
                shift
                ;;
            --aider-model)
                if [[ -n "${2:-}" ]]; then
                    export LOKI_AIDER_MODEL="$2"
                    shift 2
                else
                    echo -e "${RED}--aider-model requires a value${NC}"
                    exit 1
                fi
                ;;
            --aider-model=*)
                export LOKI_AIDER_MODEL="${1#*=}"
                shift
                ;;
            --aider-flags)
                if [[ -n "${2:-}" ]]; then
                    export LOKI_AIDER_FLAGS="$2"
                    shift 2
                else
                    echo -e "${RED}--aider-flags requires a value${NC}"
                    exit 1
                fi
                ;;
            --aider-flags=*)
                export LOKI_AIDER_FLAGS="${1#*=}"
                shift
                ;;
            --cline-model)
                if [[ -n "${2:-}" ]]; then
                    export LOKI_CLINE_MODEL="$2"
                    shift 2
                else
                    echo -e "${RED}--cline-model requires a value${NC}"
                    exit 1
                fi
                ;;
            --cline-model=*)
                export LOKI_CLINE_MODEL="${1#*=}"
                shift
                ;;
            --parallel)
                args+=("--parallel")
                shift
                ;;
            --attempts)
                # 11.3.0 T5: N>=2 is routed to the Bun route by bin/loki; N=1 is today's behavior.
                if [[ "${2:-}" != "1" ]]; then
                    echo -e "${RED}--attempts N (2-5) needs the Bun route: run via bin/loki${NC}"
                    exit 1
                fi
                shift 2
                ;;
            --attempts=1)
                shift
                ;;
            --allow-haiku)
                # Enable Haiku for the fast tier. Mirrors the LOKI_ALLOW_HAIKU=true
                # env var (consumed by providers/claude.sh and run.sh). Documented in
                # loki --help and run.sh; previously only the env var worked here, so
                # `loki start ./prd.md --allow-haiku` aborted with "Unknown option".
                # Export reaches the runner; also forward as an arg so the run.sh
                # parser (run.sh:15015) sees it on every route.
                export LOKI_ALLOW_HAIKU=true
                args+=("--allow-haiku")
                shift
                ;;
            --regen-prd|--regenerate-prd|--regen|--fresh-prd)
                # v7.8.1: force a fresh generated PRD on a no-PRD run, overriding
                # the staleness-aware reuse (decide_generated_prd_action in
                # run.sh reads LOKI_PRD_REGEN). Exported so the runner sees it.
                # --fresh-prd is the documented alias surfaced in the reuse
                # disclosure line ("pass --fresh-prd to regenerate").
                export LOKI_PRD_REGEN=1
                shift
                ;;
            --bg|--background)
                args+=("--bg")
                shift
                ;;
            --simple)
                export LOKI_COMPLEXITY=simple
                shift
                ;;
            --complex)
                export LOKI_COMPLEXITY=complex
                shift
                ;;
            --github)
                export LOKI_GITHUB_IMPORT=true
                shift
                ;;
            --no-dashboard)
                export LOKI_DASHBOARD=false
                shift
                ;;
            --api)
                export LOKI_START_API=true
                shift
                ;;
            --sandbox)
                export LOKI_SANDBOX_MODE=true
                shift
                ;;
            --isolation|--isolation=*)
                # Unified per-run isolation dial (rec #4). ONE canonical knob that
                # subsumes the fragmented --parallel (worktree) and --sandbox
                # (docker) flags, matching the zeroshot none/worktree/docker model
                # while keeping every existing flag working as an alias:
                #   none     -> default (in-place; exactly today's behavior)
                #   worktree -> git-worktree isolation (same mechanism as --parallel)
                #   docker   -> Docker sandbox (same mechanism as --sandbox)
                # Fail-closed on an unknown value: never silently downgrade to a
                # weaker isolation than the operator asked for.
                local _iso_val
                if [[ "$1" == --isolation=* ]]; then
                    _iso_val="${1#--isolation=}"; shift
                else
                    _iso_val="${2:-}"; shift 2 2>/dev/null || shift
                fi
                case "$_iso_val" in
                    none|"")
                        : ;;  # default: no isolation, in-place
                    worktree|wt)
                        args+=("--parallel") ;;
                    docker|sandbox)
                        export LOKI_SANDBOX_MODE=true ;;
                    *)
                        echo -e "${RED}Error: --isolation must be one of: none, worktree, docker (got '${_iso_val}')${NC}" >&2
                        return 2 ;;
                esac
                ;;
            --skip-memory)
                export LOKI_SKIP_MEMORY=true
                shift
                ;;
            --compliance)
                if [[ -n "${2:-}" ]]; then
                    export LOKI_COMPLIANCE_PRESET="$2"
                    echo -e "${CYAN}Compliance mode: $2${NC}"
                    shift 2
                else
                    echo -e "${RED}--compliance requires a preset name (default, healthcare, fintech, government)${NC}"
                    exit 1
                fi
                ;;
            --compliance=*)
                local compliance_val="${1#*=}"
                export LOKI_COMPLIANCE_PRESET="$compliance_val"
                echo -e "${CYAN}Compliance mode: $compliance_val${NC}"
                shift
                ;;
            --yes|-y)
                export LOKI_AUTO_CONFIRM=true
                shift
                ;;
            # Wall-clock cap. Accepts plain seconds or a suffix (30m, 2h) --
            # an operator setting a build timeout thinks in minutes and hours,
            # and forcing them to compute 7200 invites the off-by-3600 error.
            --max-duration)
                if [[ -n "${2:-}" ]]; then
                    local _md="$2" _mdn _mdmul=1
                    case "$_md" in
                        *s) _mdn="${_md%s}" ;;
                        *m) _mdn="${_md%m}"; _mdmul=60 ;;
                        *h) _mdn="${_md%h}"; _mdmul=3600 ;;
                        *)  _mdn="$_md" ;;
                    esac
                    if ! [[ "$_mdn" =~ ^[0-9]+$ ]] || [ "$_mdn" -eq 0 ]; then
                        echo "Error: --max-duration must be a positive duration (e.g. 5400, 90m, 2h); got '$2'" >&2
                        return 1
                    fi
                    export LOKI_MAX_DURATION=$(( _mdn * _mdmul ))
                    shift 2
                else
                    echo "Error: --max-duration requires a value (e.g. 90m)" >&2
                    return 1
                fi
                ;;
            # Verbosity. --quiet suppresses [INFO]/[STEP]; warnings and errors
            # always print, so a quiet run can still say why it failed.
            --quiet)
                export LOKI_LOG_LEVEL=warn
                shift
                ;;
            --log-level)
                if [[ -n "${2:-}" ]]; then
                    case "$2" in
                        debug|info|warn|error)
                            export LOKI_LOG_LEVEL="$2"
                            ;;
                        *)
                            echo "Error: --log-level must be debug, info, warn, or error (got '$2')" >&2
                            return 1
                            ;;
                    esac
                    shift 2
                else
                    echo "Error: --log-level requires a value (debug|info|warn|error)" >&2
                    return 1
                fi
                ;;
            --bmad-project)
                if [[ -n "${2:-}" ]]; then
                    bmad_project_path="$2"
                    shift 2
                else
                    echo -e "${RED}--bmad-project requires a path to a BMAD project directory${NC}"
                    exit 1
                fi
                ;;
            --bmad-project=*)
                bmad_project_path="${1#*=}"
                shift
                ;;
            --openspec)
                if [[ -n "${2:-}" ]]; then
                    openspec_change_path="$2"
                    shift 2
                else
                    echo -e "${RED}--openspec requires a path to an OpenSpec change directory${NC}"
                    exit 1
                fi
                ;;
            --openspec=*)
                openspec_change_path="${1#*=}"
                shift
                ;;
            --mirofish)
                if _loki_start_optval_consumes "${2:-}"; then
                    mirofish_url="$2"
                    shift 2
                else
                    mirofish_url="http://localhost:5001"
                    shift
                fi
                ;;
            --mirofish=*)
                mirofish_url="${1#*=}"
                shift
                ;;
            --mirofish-docker)
                if [[ -n "${2:-}" ]]; then
                    mirofish_docker_image="$2"
                    mirofish_url="${mirofish_url:-http://localhost:5001}"
                    shift 2
                else
                    echo -e "${RED}--mirofish-docker requires a Docker image name${NC}"
                    exit 1
                fi
                ;;
            --mirofish-docker=*)
                mirofish_docker_image="${1#*=}"
                mirofish_url="${mirofish_url:-http://localhost:5001}"
                shift
                ;;
            --mirofish-rounds)
                if [[ -n "${2:-}" ]]; then
                    export LOKI_MIROFISH_ROUNDS="$2"
                    shift 2
                else
                    echo -e "${RED}--mirofish-rounds requires a number${NC}"
                    exit 1
                fi
                ;;
            --mirofish-rounds=*)
                export LOKI_MIROFISH_ROUNDS="${1#*=}"
                shift
                ;;
            --mirofish-timeout)
                if [[ -n "${2:-}" ]]; then
                    export LOKI_MIROFISH_TIMEOUT="$2"
                    shift 2
                else
                    echo -e "${RED}--mirofish-timeout requires seconds${NC}"
                    exit 1
                fi
                ;;
            --mirofish-timeout=*)
                export LOKI_MIROFISH_TIMEOUT="${1#*=}"
                shift
                ;;
            --mirofish-bg)
                mirofish_bg=true
                shift
                ;;
            --no-mirofish)
                mirofish_disabled=true
                shift
                ;;
            --no-plan)
                no_plan=true
                shift
                ;;
            --fast|--repo-fast)
                # Fail closed on a conflict, same doctrine as --isolation below:
                # never silently downgrade to weaker verification than asked for.
                # --full wins regardless of order, so `--fast --full` and
                # `--full --fast` both keep the gates ON.
                if [ "$repo_depth_explicit" != "full" ]; then
                    repo_depth="fast"
                    repo_depth_explicit="fast"
                fi
                shift
                ;;
            --full|--repo-full)
                repo_depth="full"
                repo_depth_explicit="full"
                shift
                ;;
            --brief)
                # R7: explicit one-line brief (escape hatch for single-word
                # briefs that detect_arg_type would otherwise treat as a PRD
                # path). Forces the zero-config first-run brief sub-path.
                if [[ -n "${2:-}" ]] && [[ "${2:-}" != --* ]]; then
                    brief_text="$2"
                    shift 2
                else
                    echo -e "${RED}--brief requires a one-line description (e.g., --brief \"build a todo app\")${NC}"
                    exit 1
                fi
                ;;
            --brief=*)
                brief_text="${1#*=}"
                shift
                ;;
            --budget)
                if [[ -n "${2:-}" ]]; then
                    if ! echo "$2" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
                        echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget 5.00)${NC}"
                        exit 1
                    fi
                    # Reject a non-positive budget: a 0 (or 0.00) cap makes
                    # check_budget_limit pause before any work runs (cost >= 0 is
                    # always true), which looks like a silent hang. Require > 0.
                    if ! awk -v b="$2" 'BEGIN{exit !(b+0 > 0)}'; then
                        echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
                        exit 1
                    fi
                    export LOKI_BUDGET_LIMIT="$2" LOKI_BUDGET_SOURCE=flag
                    shift 2
                else
                    echo -e "${RED}--budget requires a USD amount (e.g., --budget 5.00)${NC}"
                    exit 1
                fi
                ;;
            --budget=*)
                local budget_val="${1#*=}"
                if ! echo "$budget_val" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
                    echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget=5.00)${NC}"
                    exit 1
                fi
                # Reject a non-positive budget (see --budget arm above).
                if ! awk -v b="$budget_val" 'BEGIN{exit !(b+0 > 0)}'; then
                    echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
                    exit 1
                fi
                export LOKI_BUDGET_LIMIT="$budget_val" LOKI_BUDGET_SOURCE=flag
                shift
                ;;
            # TEN-COST-01: opt-in cost profile; fills only unset levers (explicit env wins).
            --haiku)
                loki_apply_haiku_optin --haiku
                shift
                ;;
            --cost|--cost=*)
                if [[ "$1" == --cost=* ]]; then _cost_val="${1#*=}"; _cost_n=1; else _cost_val="${2:-}"; _cost_n=2; fi
                case "$_cost_val" in
                    lean) loki_lean_levers
                          export LOKI_COST_PROFILE=lean
                          echo "cost profile lean: LOKI_IMPLEMENT_HAIKU_FIRST=$LOKI_IMPLEMENT_HAIKU_FIRST LOKI_E10_TRIM=$LOKI_E10_TRIM LOKI_E10_SMALL_NO_RUNNER=$LOKI_E10_SMALL_NO_RUNNER LOKI_E10_SMALL_MAX_CHARS=$LOKI_E10_SMALL_MAX_CHARS" ;;
                    off) ;;
                    *) echo -e "${RED}--cost must be lean or off${NC}" >&2; exit 2 ;;
                esac
                shift "$_cost_n"
                ;;
            # v6.84.0: explicit mode flags (override auto-detection)
            --prd)
                if [[ -n "${2:-}" ]] && [[ "${2:-}" != --* ]]; then
                    explicit_mode="prd"
                    prd_file="$2"
                    shift 2
                else
                    echo -e "${RED}--prd requires a file path${NC}"
                    exit 1
                fi
                ;;
            --prd=*)
                explicit_mode="prd"
                prd_file="${1#*=}"
                shift
                ;;
            --issue)
                if [[ -n "${2:-}" ]] && [[ "${2:-}" != --* ]]; then
                    explicit_mode="issue"
                    issue_ref_explicit="$2"
                    shift 2
                else
                    echo -e "${RED}--issue requires a URL, number, or issue key${NC}"
                    exit 1
                fi
                ;;
            --issue=*)
                explicit_mode="issue"
                issue_ref_explicit="${1#*=}"
                shift
                ;;
            # v6.84.0: issue-mode flags (only meaningful when input is an issue)
            --dry-run)
                issue_mode_args+=("--dry-run")
                shift
                ;;
            --no-start)
                issue_mode_args+=("--no-start")
                shift
                ;;
            --output)
                if [[ -n "${2:-}" ]]; then
                    issue_output_file="$2"
                    issue_mode_args+=("--output" "$2")
                    shift 2
                else
                    echo -e "${RED}--output requires a file path${NC}"
                    exit 1
                fi
                ;;
            --output=*)
                issue_output_file="${1#*=}"
                issue_mode_args+=("--output=${1#*=}")
                shift
                ;;
            --worktree|-w)
                issue_use_worktree=true
                issue_mode_args+=("--worktree")
                shift
                ;;
            --pr)
                issue_use_worktree=true
                issue_create_pr=true
                issue_mode_args+=("--pr")
                shift
                ;;
            --prepare-pr)
                # Feature 1: the SAME `loki start <issue>` entrypoint carries the
                # prepare-only flag through to cmd_run. Does not set
                # issue_create_pr -- preparing is not publishing.
                issue_use_worktree=true
                issue_mode_args+=("--prepare-pr")
                shift
                ;;
            --ship)
                issue_use_worktree=true
                issue_create_pr=true
                issue_auto_merge=true
                issue_mode_args+=("--ship")
                shift
                ;;
            --detach|-d)
                issue_use_worktree=true
                issue_run_detached=true
                issue_mode_args+=("--detach")
                shift
                ;;
            # #691: unified config-file aliases. Already applied by the main()
            # pre-pass (loki_maybe_apply_config_file); these arms only consume
            # the flag+path so the loop does not misread the path as the PRD
            # positional, and do NOT forward it to the run.sh exec target.
            --config|--vars|--env-file)
                # Guard the shift 2: a dangling flag (path omitted, flag is the
                # last arg) would underflow shift 2 and, under set -euo pipefail,
                # abort the command SILENTLY. Give an honest actionable error.
                if [ "$#" -ge 2 ]; then
                    shift 2
                else
                    echo "loki: $1 requires a file path (e.g. --config .loki/config.yaml)" >&2
                    exit 1
                fi
                ;;
            --config=*|--vars=*|--env-file=*)
                shift
                ;;
            --remote)
                if [ "$#" -ge 2 ]; then
                    remote_url="$2"
                    shift 2
                else
                    echo "loki: --remote requires a URL (e.g. --remote https://loki.example.com)" >&2
                    exit 1
                fi
                ;;
            --remote=*)
                remote_url="${1#--remote=}"
                shift
                ;;
            --)
                # End-of-options: everything after is positional. Lets a caller
                # pass an untrusted spec that might start with '-' safely, e.g.
                # `loki start -- "$spec"` from the queue consumer, so a crafted
                # work item can never be parsed as a flag.
                shift
                if [ -z "$positional_arg" ] && [ "$#" -ge 1 ]; then
                    positional_arg="$1"
                fi
                break
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                exit 64
                ;;
            *)
                # Capture first positional arg for unified mode detection.
                # Additional positional args are ignored (same as prior behavior).
                if [ -z "$positional_arg" ]; then
                    positional_arg="$1"
                fi
                shift
                ;;
        esac
    done

    # No task and no git repo: do not ask for a provider and do not spend.
    # The offline receipt (same sample as loki tour) is the first thing they see.
    # An explicit provider (--provider or LOKI_PROVIDER) keeps its preflight, so a
    # missing or unknown provider still fails loudly instead of showing a sample.
    if [ -z "$brief_text" ] && [ -z "$prd_file" ] && [ -z "$positional_arg" ] \
       && [ -z "$provider" ] && [ -z "${LOKI_PROVIDER:-}" ] \
       && [ -z "$explicit_mode" ] && [ -z "${LOKI_PRD_FILE:-}" ] \
       && [ -z "$remote_url" ] && [ -z "${_ps_ws:-}" ] && [ "${_ps_deploy:-0}" != 1 ] \
       && [ -z "${bmad_project_path:-}" ] && [ -z "${openspec_change_path:-}" ]; then
        if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
            echo "No repo and no task. Showing a real receipt (no key, no spend)."
            echo ""
            wow_offline_finish
            return $?
        fi
    fi

    # --remote: submit to a deployed cluster instead of building locally.
    #
    # Branches HERE, after arg parsing but BEFORE the provider pre-flight and
    # provider_offer_gate below: those require a provider CLI on PATH, and the
    # whole point of a remote submit is that this machine is not doing the
    # work. Gating a remote submit on a local provider install would defeat it.
    if [ -n "$remote_url" ]; then
        local _remote_spec="${brief_text:-${prd_file:-$positional_arg}}"
        loki_remote_submit "$remote_url" "$_remote_spec"
        return $?
    fi

    # Explicit-provider pre-flight. Runs BEFORE provider_offer_gate below, and
    # is deliberately narrower than it.
    #
    # provider_offer_gate passes when ANY provider is on PATH, so `--provider
    # codex` on a claude-only machine sails through it and dies much later in
    # the runner. And when NOTHING is installed the gate offers to install
    # CLAUDE, which is a nudge toward substituting a provider the user did not
    # ask for. An explicit choice that cannot be honoured is an error, never a
    # silent (or suggested) swap: running a different model than the one asked
    # for is worse than failing.
    #
    # Only guards an EXPLICIT choice (--provider flag or LOKI_PROVIDER). The
    # default path returns immediately and still gets the friendly install
    # offer below. The later check at the "Pre-flight: check that the provider
    # CLI is installed" block stays: it covers effective_provider, which
    # includes the default, so the two have different scopes.
    local _pf_explicit="${provider:-${LOKI_PROVIDER:-}}"
    if [ -n "$_pf_explicit" ] && ! command -v "$_pf_explicit" >/dev/null 2>&1; then
        # Provider name == binary name here, same assumption as the existing
        # check below and provider-offer.sh:53. opencode is included; it is in
        # SUPPORTED_PROVIDERS but missing from the older lists.
        local _pf_installed=""
        local _pf_p
        # Single source: SUPPORTED_PROVIDERS in providers/loader.sh.
        local _pf_known
        _pf_known="$(sed -n 's/^SUPPORTED_PROVIDERS=(\(.*\))$/\1/p' "$_LOKI_SCRIPT_DIR/../providers/loader.sh" 2>/dev/null | tr -d '"' | head -1)"
        [ -n "$_pf_known" ] || _pf_known="claude codex cline aider opencode"
        case " $_pf_known " in
            *" $_pf_explicit "*) ;;
            *)
                echo -e "${RED}Error: unknown provider '$_pf_explicit' (use one of: ${_pf_known// /, })${NC}" >&2
                exit 1
                ;;
        esac
        for _pf_p in $_pf_known; do
            if command -v "$_pf_p" >/dev/null 2>&1; then
                _pf_installed="${_pf_installed:+$_pf_installed }$_pf_p"
            fi
        done

        echo -e "${RED}Error: you asked for provider '$_pf_explicit', but its CLI is not installed.${NC}" >&2
        echo "" >&2
        echo "Install it:" >&2
        case "$_pf_explicit" in
            claude)   echo "  npm install -g @anthropic-ai/claude-code" >&2 ;;
            codex)    echo "  npm install -g @openai/codex" >&2 ;;
            cline)    echo "  npm install -g cline" >&2 ;;
            aider)    echo "  pip install aider-chat" >&2 ;;
            opencode) echo "  npm install -g opencode-ai" >&2 ;;
            *)        echo "  No install command is known for '$_pf_explicit'." >&2 ;;
        esac
        echo "" >&2
        if [ -n "$_pf_installed" ]; then
            echo "Installed providers on this machine: $_pf_installed" >&2
            echo "Re-run with one of those, e.g. loki start --provider ${_pf_installed%% *}" >&2
        else
            echo "No supported provider CLI is installed on this machine." >&2
            echo "Supported providers: claude, codex, cline, aider, opencode" >&2
        fi
        echo "" >&2
        echo "Loki will not silently run a different provider than the one you asked for." >&2
        exit 1
    fi

    # v7.29.0: provider pre-flight gate. If no provider CLI is installed, offer
    # to install one (TTY) or print honest manual instructions and exit 2
    # (non-TTY/CI), BEFORE any spend or runner entry. This moves what used to be
    # an opaque deep-runner failure to a friendly, actionable top-of-run gate.
    # --help already returned inside the arg loop, so we never gate help.
    if declare -f provider_offer_gate >/dev/null 2>&1; then
        if ! provider_offer_gate; then
            exit 2
        fi
    fi

    # One line each. Printed on the path that will spend, not on --help or the
    # empty non-repo receipt above. The Loki 10 engine path prints the same
    # lines from bin/loki and does not reach here.
    loki_print_autonomy "${positional_arg:-}"
    loki_f2p_banner

    # Clear any stale raw-brief marker from a PRIOR run before we decide the
    # mode of THIS run. Only the brief branch (below) re-writes it. Without this,
    # a brief run leaves .loki/state/brief.txt behind and a later non-brief run
    # (PRD or codebase-analysis) would have its proof mislabel source="brief"
    # with the old one-liner, since proof-generator reads brief.txt first.
    rm -f "$LOKI_DIR/state/brief.txt" 2>/dev/null || true

    # v6.84.0: Unified dispatch based on explicit flags or auto-detection
    # Precedence: --brief > --issue > --prd > positional auto-detect >
    #             LOKI_PRD_FILE env
    local detected_type=""
    if [ -n "$brief_text" ]; then
        # R7: explicit --brief always forces the zero-config brief sub-path.
        detected_type="brief"
    elif [ -n "$explicit_mode" ]; then
        detected_type="$explicit_mode"
    elif [ -n "$positional_arg" ]; then
        detected_type=$(detect_arg_type "$positional_arg")
        # R7: a positional one-line brief (whitespace, not a file/issue) maps to
        # the brief sub-path; capture the text from the positional arg.
        if [ "$detected_type" = "brief" ]; then
            brief_text="$positional_arg"
        fi
    elif [ -n "${LOKI_PRD_FILE:-}" ]; then
        detected_type="prd"
        prd_file="$LOKI_PRD_FILE"
    else
        detected_type="empty"
    fi

    # Resolve the directory while the positional path is still relative to the
    # caller, then enter it before creating any .loki state. Quoted `cd --`
    # keeps spaces and shell metacharacters inert.
    if [ "$detected_type" = "repo" ]; then
        local repo_target
        repo_target=$(cd -- "$positional_arg" 2>/dev/null && pwd -P) || {
            echo -e "${RED}Error: cannot enter repository directory: $positional_arg${NC}" >&2
            return 1
        }
        cd -- "$repo_target" || return 1
        if [ "$LOKI_DIR" = ".loki" ]; then
            LOKI_DIR="$repo_target/.loki"
        fi
        export LOKI_REPO_TARGET="$repo_target"
    fi

    # ISSUE mode: delegate to cmd_run's code path. To avoid duplicating ~450
    # lines of issue-fetching + PR-creation logic, we re-dispatch through
    # cmd_run with reconstructed args. cmd_run then calls cmd_start again
    # with the generated PRD file and --no-plan (see line ~4431).
    if [ "$detected_type" = "issue" ]; then
        local issue_ref
        if [ "$explicit_mode" = "issue" ]; then
            issue_ref="$issue_ref_explicit"
        else
            issue_ref="$positional_arg"
        fi
        # Reconstruct args for cmd_run: issue ref + all options we captured.
        # args[] already has --provider, --parallel, --bg, etc.
        local run_args=("$issue_ref")
        # Pass through issue-mode flags
        for a in "${issue_mode_args[@]+"${issue_mode_args[@]}"}"; do
            run_args+=("$a")
        done
        # Pass through generic options that cmd_run understands
        for a in "${args[@]+"${args[@]}"}"; do
            run_args+=("$a")
        done
        # Pass through --no-plan if set
        if [ "$no_plan" = "true" ]; then
            run_args+=("--no-plan")
        fi
        # Mark this invocation as unified (so cmd_run skips deprecation notice)
        export LOKI_UNIFIED_START=1
        cmd_run "${run_args[@]}"
        return $?
    fi

    # BRIEF + isolation flags (--pr/--worktree/--detach/--prepare-pr/--ship):
    # the flags were collected above but only the issue path consumed them, so a
    # delegated brief ran in the caller's checkout with no PR. Route it through
    # cmd_run (worktree, detach, receipt-bearing PR) as a tracker-less issue.
    if [ "$detected_type" = "brief" ] && [ "$issue_use_worktree" = "true" ]; then
        local brief_run_args=("--brief-text" "$brief_text")
        for a in "${issue_mode_args[@]+"${issue_mode_args[@]}"}"; do
            brief_run_args+=("$a")
        done
        for a in "${args[@]+"${args[@]}"}"; do
            brief_run_args+=("$a")
        done
        brief_run_args+=("--no-plan")
        export LOKI_UNIFIED_START=1
        cmd_run "${brief_run_args[@]}"
        return $?
    fi

    # R7 (zero-config first run): BRIEF mode. The user gave a one-line brief
    # (`loki start "build a todo app"` or `loki start --brief "..."`). Synthesize
    # a forward-looking PRD, switch to the lightweight TTFV profile (honest fast:
    # capped iterations, council off, simple tier, heavy phases off), and mark
    # LOKI_TTFV=1 so run.sh prints the "what next / go deeper" framing at the
    # end. Then fall through to the normal flow, which appends prd_file to args
    # and execs run.sh. This is additive -- nothing here changes the PRD/issue
    # paths above.
    if [ "$detected_type" = "brief" ]; then
        local version
        version=$(get_version)
        local _ttfv_max_iter="${LOKI_MAX_ITERATIONS:-3}"
        mkdir -p "$LOKI_DIR" 2>/dev/null || true
        # Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
        # into run.sh, so these are never cleaned on exit and accumulate).
        find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
        find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
        local brief_prd="$LOKI_DIR/brief-prd-$$.md"
        synthesize_brief_prd "$brief_prd" "$brief_text"
        prd_file="$brief_prd"

        # Persist the raw one-liner so the proof-of-run can show the actual brief
        # the user typed ("Built and verified from: <brief>") instead of falling
        # back to "codebase-analysis / No brief recorded". The synthesized PRD is
        # kept distinct (brief-prd-$$.md); the raw brief is the stronger, more
        # honest shareable artifact. proof-generator.py::_collect_spec reads this
        # as a fallback when no PRD_PATH / generated-prd.md is present.
        mkdir -p "$LOKI_DIR/state" 2>/dev/null || true
        printf '%s' "$brief_text" > "$LOKI_DIR/state/brief.txt" 2>/dev/null || true

        # Apply the shared lightweight profile and flag the TTFV first-run path.
        # The signal value ("brief") drives the end-of-run wording in run.sh so
        # the message matches what actually ran (lightweight, council off).
        set_ttfv_lightweight_profile "$_ttfv_max_iter"
        export LOKI_TTFV=brief
        # Skip the heavy auto-plan analysis -- the brief path is the fast path,
        # and we already printed the upfront framing below.
        no_plan=true

        echo -e "${BOLD}Loki Mode v$version - Zero-config first run${NC}"
        echo ""
        echo -e "${CYAN}Brief:${NC} $brief_text"
        echo -e "${DIM}Fast first pass: $_ttfv_max_iter iterations max, council off, simple tier.${NC}"
        echo -e "${DIM}You will get a runnable artifact and a proof-of-run quickly.${NC}"
        echo -e "${DIM}Go deeper later with: loki start (full RARV-C depth).${NC}"
        echo -e "${GREEN}Brief PRD written${NC} to $brief_prd"
        echo ""
    fi

    # REPO mode: `loki start <directory>`. We already cd'd into the target
    # above, so this is the no-arg codebase path with one difference -- a
    # synthesized brief that bounds the work to ONE high-value improvement
    # instead of "build out the whole project".
    #
    # The DEFAULT and `--full` deliberately do NOT call
    # set_ttfv_lightweight_profile: that helper turns the council and the
    # code-review phase OFF, and the whole point of this entry point is to hand
    # back VERIFIED evidence. Scope is bounded by the brief text, not by
    # disabling the gates that check the work. Those runs keep full depth and
    # report LOKI_TTFV=repo -- the same honest full-depth signal the no-arg path
    # uses.
    #
    # `--fast` is the opt-in exception: it applies the SAME lightweight profile
    # the brief path uses (capped iterations, council off, simple tier, heavy
    # phases off) and reports LOKI_TTFV=repo-fast, a distinct signal so the
    # end-of-run wording in run.sh cannot claim council verdicts that never ran.
    if [ "$detected_type" = "repo" ]; then
        local version
        version=$(get_version)
        # Same cap the brief sub-path reads, so "fast" means one thing.
        local _repo_max_iter="${LOKI_MAX_ITERATIONS:-3}"
        mkdir -p "$LOKI_DIR" 2>/dev/null || true
        find "$LOKI_DIR" -maxdepth 1 -name 'repo-prd-*.md' -mtime +1 -delete 2>/dev/null || true
        local repo_prd="$LOKI_DIR/repo-prd-$$.md"
        synthesize_repo_prd "$repo_prd" "$repo_depth"
        prd_file="$repo_prd"

        if [ "$repo_depth" = "fast" ]; then
            set_ttfv_lightweight_profile "$_repo_max_iter"
            export LOKI_TTFV=repo-fast
            no_plan=true
        fi

        echo -e "${BOLD}Loki Mode v$version - Repo improvement${NC}"
        echo ""
        echo -e "${CYAN}Repo:${NC} $LOKI_REPO_TARGET"
        if [ "$repo_depth" = "fast" ]; then
            echo -e "${DIM}Loki will inspect this project, pick one high-value improvement,${NC}"
            echo -e "${DIM}implement it, and test it.${NC}"
            echo -e "${DIM}Fast pass: $_repo_max_iter iterations max, simple tier, council and${NC}"
            echo -e "${DIM}heavy phases OFF. Reduced gates: you get measured run evidence${NC}"
            echo -e "${DIM}(diffs, cost, time), NOT a council-verified verdict.${NC}"
            echo -e "${DIM}Run full verification with: loki start \"$LOKI_REPO_TARGET\" --full${NC}"
        else
            echo -e "${DIM}Loki will inspect this project, pick one high-value improvement,${NC}"
            echo -e "${DIM}implement it, test it, and produce verified evidence.${NC}"
            echo -e "${DIM}Full depth: quality gates and completion council stay ON.${NC}"
        fi
        echo -e "${DIM}Scope it yourself instead with: loki start \"<what you want>\".${NC}"
        echo ""
    fi

    # R7: existing-repo no-arg path also gets the TTFV "what next" framing at the
    # end of the run. Execution here is UNCHANGED (full-depth no-PRD codebase
    # analysis + generated-PRD-reuse); we only add accurate end-of-run framing.
    # The signal value ("repo") drives the full-depth wording in run.sh so the
    # message does not falsely claim a lightweight pass.
    if { [ "$detected_type" = "empty" ] || [ "$detected_type" = "repo" ]; } \
       && [ -z "${LOKI_TTFV:-}" ]; then
        export LOKI_TTFV=repo
    fi

    # PRD mode: positional arg is the PRD file (if not already set via --prd)
    if [ "$detected_type" = "prd" ] && [ -z "$prd_file" ]; then
        prd_file="$positional_arg"
    fi
    # Unknown type: fall back to PRD (legacy back-compat)
    if [ "$detected_type" = "unknown" ] && [ -z "$prd_file" ]; then
        prd_file="$positional_arg"
    fi

    # R8 fix (v7.0.0): if a PRD path was specified (positional or --prd) and
    # the file does NOT exist, fail fast with a clear error. This prevents the
    # "silently accepted, then run as no-PRD" failure mode.
    if [ -n "$prd_file" ] && [ "$detected_type" != "empty" ]; then
        case "$prd_file" in
            *.md|*.json|*.txt|*.yaml|*.yml)
                if [ ! -f "$prd_file" ]; then
                    _prd_not_found_die "$prd_file"
                fi
                ;;
        esac
    fi

    # Support LOKI_PRD_FILE environment variable as fallback
    if [ -z "$prd_file" ] && [ -n "${LOKI_PRD_FILE:-}" ]; then
        prd_file="$LOKI_PRD_FILE"
    fi

    # v6.84.1 (R8 Gap 1): fail fast if a PRD-like path was provided but the file
    # does not exist on disk. Previously we silently fell through and let the
    # "no PRD specified" branch take over, which misled users who typo'd a path.
    # Only applies when an explicit path was given (positional, --prd, or env);
    # the no-arg "generate from codebase" path is still supported below.
    # BMAD/OpenSpec adapters run later and may set prd_file to a normalized
    # output that doesn't exist yet -- they're guarded by bmad_project_path /
    # openspec_change_path, so skip this check when either is set.
    if [ -n "$prd_file" ] && [ -z "$bmad_project_path" ] && [ -z "$openspec_change_path" ]; then
        case "$prd_file" in
            *.md|*.json|*.txt|*.yaml|*.yml)
                if [ ! -f "$prd_file" ]; then
                    _prd_not_found_die "$prd_file"
                fi
                ;;
        esac
    fi

    # Mutual exclusivity: --openspec and --bmad-project cannot be used together
    if [[ -n "${openspec_change_path:-}" ]] && [[ -n "${bmad_project_path:-}" ]]; then
        echo -e "${RED}Error: --openspec and --bmad-project are mutually exclusive. Use one or the other.${NC}"
        exit 1
    fi

    # BMAD project validation and adapter execution
    if [[ -n "$bmad_project_path" ]]; then
        # Resolve to absolute path
        if [[ ! "$bmad_project_path" = /* ]]; then
            local original_bmad_path="$bmad_project_path"
            bmad_project_path="$(cd "$bmad_project_path" 2>/dev/null && pwd)" || {
                echo -e "${RED}Error: BMAD project path does not exist: $original_bmad_path${NC}"
                exit 1
            }
        fi

        # Validate path is a directory
        if [[ ! -d "$bmad_project_path" ]]; then
            echo -e "${RED}Error: BMAD project path is not a directory: $bmad_project_path${NC}"
            exit 1
        fi

        # Check for BMAD artifacts (_bmad-output/ or common BMAD markers)
        if [[ ! -d "$bmad_project_path/_bmad-output" ]]; then
            echo -e "${YELLOW}Warning: No _bmad-output/ directory found in $bmad_project_path${NC}"
            echo "Expected a BMAD Method project with _bmad-output/ artifacts."
            echo -e "Continue anyway? [y/N] \\c"
            local _auto="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
            if [[ "$_auto" == "true" ]]; then
                echo "y (auto-confirmed)"
            else
                read -r _bmad_confirm
                if [[ ! "$_bmad_confirm" =~ ^[Yy] ]]; then
                    echo "Aborted."
                    exit 0
                fi
            fi
        fi

        # Export for run.sh to access
        export BMAD_PROJECT_PATH="$bmad_project_path"

        # Ensure .loki directory exists for adapter output
        mkdir -p "$LOKI_DIR"

        # Run the BMAD adapter to normalize artifacts
        echo -e "${CYAN}Running BMAD adapter...${NC}"
        local adapter_script="$(dirname "$(resolve_script_path "$0")")/bmad-adapter.py"
        if [[ ! -f "$adapter_script" ]]; then
            echo -e "${RED}Error: BMAD adapter not found at $adapter_script${NC}"
            echo "Please ensure autonomy/bmad-adapter.py exists."
            exit 1
        fi

        if ! python3 "$adapter_script" "$bmad_project_path" --output-dir "$LOKI_DIR" --validate; then
            echo -e "${RED}Error: BMAD adapter failed. Check the project artifacts.${NC}"
            exit 1
        fi
        echo -e "${GREEN}BMAD artifacts normalized successfully.${NC}"

        # If no explicit PRD was provided, use the normalized BMAD PRD
        if [[ -z "$prd_file" ]] && [[ -f "$LOKI_DIR/bmad-prd-normalized.md" ]]; then
            prd_file="$LOKI_DIR/bmad-prd-normalized.md"
            echo -e "${CYAN}Using normalized BMAD PRD: $prd_file${NC}"
        fi
    fi

    # OpenSpec change directory validation and adapter execution
    if [[ -n "$openspec_change_path" ]]; then
        # Resolve to absolute path
        if [[ ! "$openspec_change_path" = /* ]]; then
            local original_openspec_path="$openspec_change_path"
            openspec_change_path="$(cd "$openspec_change_path" 2>/dev/null && pwd)" || {
                echo -e "${RED}Error: OpenSpec change path does not exist: $original_openspec_path${NC}"
                exit 1
            }
        fi

        # Validate path is a directory
        if [[ ! -d "$openspec_change_path" ]]; then
            echo -e "${RED}Error: OpenSpec change path is not a directory: $openspec_change_path${NC}"
            exit 1
        fi

        # Check for required OpenSpec artifacts
        if [[ ! -f "$openspec_change_path/proposal.md" ]]; then
            echo -e "${RED}Error: No proposal.md found in $openspec_change_path${NC}"
            echo "Expected an OpenSpec change directory with proposal.md and specs/."
            exit 1
        fi

        # Export for run.sh to access
        export OPENSPEC_CHANGE_PATH="$openspec_change_path"

        # Ensure .loki directory exists for adapter output
        mkdir -p "$LOKI_DIR"

        # Run the OpenSpec adapter to normalize artifacts
        echo -e "${CYAN}Running OpenSpec adapter...${NC}"
        local adapter_script="$(dirname "$(resolve_script_path "$0")")/openspec-adapter.py"
        if [[ ! -f "$adapter_script" ]]; then
            echo -e "${RED}Error: OpenSpec adapter not found at $adapter_script${NC}"
            echo "Please ensure autonomy/openspec-adapter.py exists."
            exit 1
        fi

        # Validate first
        if ! python3 "$adapter_script" "$openspec_change_path" --validate; then
            echo -e "${RED}Error: OpenSpec adapter validation failed. Check the change artifacts.${NC}"
            exit 1
        fi

        # Generate output files
        if ! python3 "$adapter_script" "$openspec_change_path" --output-dir "$LOKI_DIR"; then
            echo -e "${RED}Error: OpenSpec adapter failed to generate output files.${NC}"
            exit 1
        fi
        echo -e "${GREEN}OpenSpec artifacts normalized successfully.${NC}"

        # If no explicit PRD was provided, use the normalized OpenSpec PRD
        if [[ -z "$prd_file" ]] && [[ -f "$LOKI_DIR/openspec-prd-normalized.md" ]]; then
            prd_file="$LOKI_DIR/openspec-prd-normalized.md"
            echo -e "${CYAN}Using normalized OpenSpec PRD: $prd_file${NC}"
        fi
    fi

    # MiroFish market validation (optional, non-blocking)
    if [[ -z "$mirofish_url" ]] && [[ -n "${LOKI_MIROFISH_URL:-}" ]] && [[ "$mirofish_disabled" != "true" ]]; then
        mirofish_url="$LOKI_MIROFISH_URL"
    fi

    if [[ -n "$mirofish_url" ]] && [[ "$mirofish_disabled" != "true" ]]; then
        export MIROFISH_URL="$mirofish_url"
        mkdir -p "$LOKI_DIR"

        local mf_adapter="$(dirname "$(resolve_script_path "$0")")/mirofish-adapter.py"
        if [[ ! -f "$mf_adapter" ]]; then
            echo -e "${RED}Error: MiroFish adapter not found at $mf_adapter${NC}"
            exit 1
        fi

        # Docker auto-start if requested
        if [[ -n "$mirofish_docker_image" ]]; then
            echo -e "${CYAN}Starting MiroFish container...${NC}"
            if ! python3 "$mf_adapter" --docker-start --docker-image "$mirofish_docker_image" \
                    ${LOKI_MIROFISH_PORT:+--port "$LOKI_MIROFISH_PORT"}; then
                echo -e "${RED}Error: Failed to start MiroFish container${NC}"
                exit 1
            fi
        fi

        # Health check
        if ! python3 "$mf_adapter" --health --url "$mirofish_url" 2>/dev/null; then
            echo -e "${YELLOW}Warning: MiroFish not reachable at $mirofish_url${NC}"
            echo "Start MiroFish with: loki start --mirofish-docker <image>"
            echo -e "Continue without market validation? [y/N] \\c"
            local _auto="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
            if [[ "$_auto" == "true" ]]; then
                echo "y (auto-confirmed)"
            else
                read -r _mf_confirm
                if [[ ! "$_mf_confirm" =~ ^[Yy] ]]; then
                    exit 0
                fi
            fi
            unset MIROFISH_URL
        else
            echo -e "${GREEN}MiroFish reachable at $mirofish_url${NC}"

            # Need a PRD to run MiroFish
            local _mf_prd="${prd_file:-}"
            if [[ -z "$_mf_prd" ]]; then
                echo -e "${YELLOW}Warning: No PRD file -- skipping MiroFish (needs PRD for simulation seed)${NC}"
                unset MIROFISH_URL
            else
                # Validate PRD can be parsed for MiroFish
                if python3 "$mf_adapter" "$_mf_prd" --validate --url "$mirofish_url" 2>/dev/null; then
                    echo -e "${GREEN}PRD validated for MiroFish simulation${NC}"
                    # Run with --background (adapter forks internally, returns immediately)
                    python3 "$mf_adapter" "$_mf_prd" --output-dir "$LOKI_DIR" \
                        --url "$mirofish_url" --background \
                        ${LOKI_MIROFISH_ROUNDS:+--max-rounds "$LOKI_MIROFISH_ROUNDS"} \
                        ${LOKI_MIROFISH_TIMEOUT:+--timeout "$LOKI_MIROFISH_TIMEOUT"}
                    # PID is tracked in .loki/mirofish/pipeline-state.json by the adapter
                    echo -e "${CYAN}MiroFish pipeline started in background. Status: $LOKI_DIR/mirofish/pipeline-state.json${NC}"
                else
                    echo -e "${YELLOW}Warning: PRD validation for MiroFish failed. Continuing without.${NC}"
                    unset MIROFISH_URL
                fi
            fi
        fi
    fi

    if [ -n "$prd_file" ]; then
        args+=("$prd_file")
    else
        # No PRD file specified -- warn and confirm before starting
        # Auto-confirm in CI environments or when LOKI_AUTO_CONFIRM is set
        # LOKI_AUTO_CONFIRM takes precedence when explicitly set;
        # fall back to CI env var only when LOKI_AUTO_CONFIRM is unset.
        # v7.7.13 fix (user-reported Docker bug): also auto-confirm when stdin
        # is not a TTY. Without this, `docker run --rm asklokesh/loki-mode
        # start` showed the prompt then exited because the user didn't pass
        # `-it` to docker, so stdin was closed and `read` returned EOF with
        # an empty string; the script kept going but the user only saw the
        # prompt before the next bug fired. Now non-TTY callers get a clean
        # auto-confirm path identical to CI.
        local _auto_confirm="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
        if [[ "$_auto_confirm" != "true" ]] && [ ! -t 0 ]; then
            _auto_confirm="true"
            echo -e "${YELLOW}Warning: No PRD file specified, stdin not a TTY (e.g. docker run without -it). Auto-confirming.${NC}"
            echo -e "${YELLOW}Tip: pass docker run -it ... or set LOKI_AUTO_CONFIRM=true to suppress this warning.${NC}"
        fi
        if [[ "$_auto_confirm" == "true" ]]; then
            echo -e "${YELLOW}Warning: No PRD file specified. Auto-confirming (CI mode).${NC}"
        else
            echo -e "${YELLOW}No PRD file specified.${NC}"
            # v7.8.1+: the runner (run_autonomous -> decide_generated_prd_action)
            # makes the real reuse|update|generate decision. Here we only adjust
            # the TTY prompt wording so it does not falsely claim "generate" when
            # a previously generated PRD already exists. --regen-prd / --fresh-prd
            # (LOKI_PRD_REGEN=1) always forces a fresh generation.
            local _prompt_q="Generate PRD from codebase and start? [Y/n] "
            if [ "${LOKI_PRD_REGEN:-}" = "1" ]; then
                echo "Loki Mode will regenerate the PRD from the codebase (forced)."
                _prompt_q="Regenerate PRD from codebase (forced) and start? [Y/n] "
            elif [ -f ".loki/generated-prd.md" ] || [ -f ".loki/generated-prd.json" ]; then
                echo "A previously generated PRD exists. Loki Mode will reuse it"
                echo "if the codebase is unchanged, update it incrementally if it"
                echo "changed, or use it as-is if you hand-edited it. Pass"
                echo "--fresh-prd to regenerate from scratch."
                _prompt_q="Continue with the existing generated PRD and start? [Y/n] "
            else
                echo "Loki Mode will analyze the existing codebase and generate"
                echo "a PRD automatically. No requirements document needed."
            fi
            echo ""
            # v7.5.3 UX: rephrased + default flipped from N to Y. The pre-v7.5.3
            # "Continue? [y/N]" with default-N caused users to accidentally
            # cancel by hitting Enter after reading the explanation.
            echo -e "$_prompt_q\c"
            read -r confirm
            if [[ "$confirm" =~ ^[Nn] ]]; then
                echo "Aborted. Usage: loki start <path-to-prd.md>"
                exit 0
            fi
        fi
    fi

    # v6.81.1: auto-show PRD analysis so users see complexity / cost / time
    # up front without needing a separate `loki plan` invocation. Skipped via
    # --no-plan or in non-TTY contexts (CI/pipe). No-op when PRD is absent.
    if [ -n "$prd_file" ]; then
        maybe_show_auto_plan "$prd_file" "$no_plan" || true
    fi

    # Load saved provider if not specified on command line
    if [ -z "$provider" ]; then
        if [ -f "$LOKI_DIR/state/provider" ]; then
            provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
            if [ -n "$provider" ]; then
                args+=("--provider" "$provider")
            fi
        fi
    fi

    # Shared-checkout concurrency guard (client bug: N `loki start ... --detach`
    # in ONE working dir with a SHARED .loki/ collide on non-namespaced worktree
    # paths + branches, producing missing worktrees and log spam). Issue-mode
    # namespaces pids PER SESSION, so the global loki.pid check below never trips
    # for DIFFERENT issues -- there was no guard on concurrent runs per checkout.
    # Detect a LIVE sibling loki running in THIS checkout via per-run pid markers
    # (crash-safe: dead markers are pruned via kill -0, so no deadlock). A run
    # that isolates its state (a distinct LOKI_DIR, i.e. NOT the default .loki) is
    # exempt -- that IS the safe per-issue pattern. Escape hatch:
    # LOKI_ALLOW_SHARED_CHECKOUT=1.
    local _co_loki_dir="${LOKI_DIR:-.loki}"
    if [ "${LOKI_ALLOW_SHARED_CHECKOUT:-0}" != "1" ] \
       && { [ -z "${LOKI_DIR:-}" ] || [ "$(cd "${LOKI_DIR}" 2>/dev/null && pwd)" = "$(cd .loki 2>/dev/null && pwd)" ]; }; then
        local _co_runs_dir="$_co_loki_dir/checkout-runs"
        mkdir -p "$_co_runs_dir" 2>/dev/null || true
        local _co_marker _co_pid _co_live=""
        for _co_marker in "$_co_runs_dir"/*; do
            [ -f "$_co_marker" ] || continue
            _co_pid=$(cat "$_co_marker" 2>/dev/null | tr -dc '0-9')
            if [ -n "$_co_pid" ] && [ "$_co_pid" != "$$" ] && kill -0 "$_co_pid" 2>/dev/null; then
                _co_live="$_co_pid"; break
            fi
            # Prune a dead marker.
            [ -n "$_co_pid" ] && rm -f "$_co_marker" 2>/dev/null || true
        done
        if [ -n "$_co_live" ]; then
            echo -e "${RED}Error: another loki run (pid $_co_live) is active in this checkout ($(pwd)).${NC}" >&2
            echo -e "${YELLOW}Running several issues at once from ONE checkout + shared .loki/ collides on worktrees and branches.${NC}" >&2
            echo -e "${YELLOW}Give each issue its own worktree AND state dir, e.g.:${NC}" >&2
            echo -e "${YELLOW}  git worktree add ../repo-issue-N -b loki/issue-N && (cd ../repo-issue-N && LOKI_DIR=\"\$PWD/.loki\" loki start owner/repo#N --pr --detach)${NC}" >&2
            echo -e "${YELLOW}See 'skills/github-integration.md' (Multiple issues at once). Override with LOKI_ALLOW_SHARED_CHECKOUT=1.${NC}" >&2
            exit 1
        fi
        # Register our own marker for the duration of this run (best-effort;
        # pruned by the next start if we die, and by 'loki stop').
        echo "$$" > "$_co_runs_dir/$$" 2>/dev/null || true
    fi

    # v7.5.12 Gap B: Stale-PID detection. Hard-kill (Ctrl+C followed by SIGKILL or
    # `loki stop`) can leave .loki/loki.pid + .loki/session.lock orphaned. The
    # next `loki start` then refuses to launch -- or worse, run.sh's downstream
    # cleanup may treat the stale pid as live. Detect-and-clean here, BEFORE
    # exec, so the user gets one clear log line instead of mysterious silent
    # behavior.
    local _start_loki_dir="${LOKI_DIR:-.loki}"
    local _start_pid_file="$_start_loki_dir/loki.pid"
    if [ -f "$_start_pid_file" ]; then
        local _existing_pid
        _existing_pid=$(cat "$_start_pid_file" 2>/dev/null | tr -dc '0-9')
        # Docker resume fix: in a `--rm` container every run starts a fresh PID
        # namespace where the orchestrator is PID 1. A prior run that wrote
        # loki.pid=1 leaves that marker in the bind-mounted .loki on the host;
        # the NEXT container is ALSO PID 1, so `kill -0 1` (a self-signal) always
        # succeeds and the lock falsely reads "another instance is running",
        # making resume in the same folder impossible. Two cases are NOT a live
        # sibling and must be cleared: (a) the recorded pid equals OUR OWN pid
        # ($$) -- this is the in-container resume case, where the new run is PID 1
        # and the stale marker is also 1; (b) the recorded pid is 1 but we are
        # NOT pid 1 (a host reading a container's leftover marker). Mirrors the
        # downstream guard in run.sh. A genuine concurrent sibling can never
        # share our $$, so this never masks a real running instance.
        if [ "$_existing_pid" = "$$" ] || { [ "$_existing_pid" = "1" ] && [ "$$" != "1" ]; }; then
            _existing_pid=""
        fi
        if [ -n "$_existing_pid" ] && kill -0 "$_existing_pid" 2>/dev/null; then
            echo -e "${RED}Error: another loki instance is running (pid $_existing_pid).${NC}" >&2
            # 'loki stop' is folder-scoped by default (v7.7.30), so it only stops a
            # run in THIS directory. If the live instance is in another project,
            # 'loki stop' here reports "No active session" and the user is stuck in
            # a start-says-running / stop-says-clean loop. Point at the global stop
            # and at 'loki status' to find the live run.
            echo -e "${YELLOW}Run 'loki stop' here, or 'loki stop --all' if it is running in another project.${NC}" >&2
            echo -e "${YELLOW}See 'loki status' for the live run, then retry 'loki start'.${NC}" >&2
            exit 1
        fi
        # PID is stale (file present but process gone). Log + remove + continue.
        echo -e "${YELLOW}Removing stale pid file ($_start_pid_file, pid=${_existing_pid:-empty} not alive)${NC}" >&2
        rm -f "$_start_pid_file" 2>/dev/null || true
        rm -f "$_start_loki_dir/session.lock" 2>/dev/null || true
    fi

    # Determine effective provider for display
    local effective_provider="${provider:-${LOKI_PROVIDER:-claude}}"

    # Handle sandbox mode - delegate to sandbox.sh
    # Skip if we're already inside a sandbox (IS_SANDBOX=1 or /.dockerenv exists)
    if [[ "${LOKI_SANDBOX_MODE:-}" == "true" ]] && [[ "${IS_SANDBOX:-}" != "1" ]] && [[ ! -f /.dockerenv ]]; then
        if [ ! -f "$SANDBOX_SH" ]; then
            echo -e "${RED}Error: sandbox.sh not found at $SANDBOX_SH${NC}"
            exit 1
        fi

        echo -e "${GREEN}Starting Loki Mode in sandbox...${NC}"
        # Load memory context before sandbox start (errors don't block startup)
        load_memory_context "$prd_file" || true
        emit_event session cli start "provider=$effective_provider" "sandbox=true" "prd_path=${prd_file:-}"
        # v7.7.13 fix: safe empty-array expansion (see line 1551 comment).
        exec "$SANDBOX_SH" start ${args[@]+"${args[@]}"}
    fi

    # Load memory context before starting (errors don't block startup)
    load_memory_context "$prd_file" || true

    # Show provider info
    echo -e "${GREEN}Starting Loki Mode...${NC}"
    echo -e "${CYAN}Provider:${NC} $effective_provider"
    if [ -f "$LOKI_DIR/state/provider" ]; then
        echo -e "${DIM}  (saved for this project - change with: loki provider set <name>)${NC}"
    else
        echo -e "${DIM}  (default - save for project with: loki provider set <name>)${NC}"
    fi
    echo ""

    # Emit session start event
    emit_event session cli start "provider=$effective_provider" "prd_path=${prd_file:-}"

    # Record session start time for efficiency tracking
    record_session_start

    # UT2-13: Write cli-provider marker when --provider was explicitly passed.
    # This lets cmd_status_json report provider_source="cli" for the lifetime
    # of this session (24-hour heuristic). Atomic write via .tmp + mv.
    # v7.7.11 (council fix): validate provider against canonical set BEFORE
    # writing so a typo like `--provider xyz` does not pollute status output
    # with bogus state for 24h. Also write current PID alongside so the reader
    # can drop the marker when the originating process is no longer alive
    # (SIGKILL / crash paths that skip our cleanup hooks).
    case "$provider" in
        claude|codex|cline|aider|opencode)
            mkdir -p "$LOKI_DIR/state" 2>/dev/null || true
            printf '%s:%s:%s\n' "$provider" "$(date +%s)" "$$" \
                > "$LOKI_DIR/state/.cli-provider.tmp" 2>/dev/null \
                && mv "$LOKI_DIR/state/.cli-provider.tmp" \
                      "$LOKI_DIR/state/cli-provider" 2>/dev/null || true
            ;;
        '') : ;;  # no --provider passed
        *)  log_warn "UT2-13: skipped cli-provider marker (unknown provider '$provider')" ;;
    esac

    # Emit learning signal: user preference for provider selection (SYN-018)
    if [ -n "$provider" ]; then
        # User explicitly chose a provider
        emit_learning_signal user_preference \
            --source cli \
            --action "provider_selection" \
            --key "provider" \
            --value "$provider" \
            --rejected '["'"$([ "$provider" != "claude" ] && echo "claude" || echo "codex")'", "'"$([ "$provider" != "cline" ] && echo "cline" || echo "aider")'"]' \
            --confidence 0.95
    fi

    # Emit learning signal: workflow pattern for session start
    emit_learning_signal workflow_pattern \
        --source cli \
        --action "session_start" \
        --workflow-name "loki_session" \
        --steps '["init", "load_memory", "start_runner"]' \
        --outcome success \
        --context "{\"provider\":\"$effective_provider\",\"prd_path\":\"${prd_file:-}\"}"

    # Pre-flight: check that the provider CLI is installed
    if ! command -v "$effective_provider" &>/dev/null; then
        echo -e "${RED}Error: Provider CLI '$effective_provider' is not installed.${NC}"
        echo ""
        echo "Install it first (same fix as loki doctor):"
        _loki_req_fix_line "$effective_provider"
        echo ""
        echo "Check your environment: loki doctor"
        exit 1
    fi

    # --api flag: start the dashboard API server before the build.
    # cmd_dashboard_start already daemonizes the server (nohup ... &) and runs
    # its own readiness probe, so run it in a contained SUBSHELL (not a
    # double-background) and surface the outcome. The old
    # `cmd_dashboard_start 2>/dev/null &` swallowed all output AND double-
    # backgrounded the already-daemonized server, so a port-in-use or startup
    # failure was invisible and the user got no dashboard and no error. The
    # subshell also contains cmd_dashboard_start's internal `exit 1` so a
    # dashboard failure can never abort the build.
    if [ "${LOKI_START_API:-false}" = "true" ]; then
        local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
        echo -e "${GREEN}Starting dashboard API on port $dash_port...${NC}"
        ( cmd_dashboard_start ) || echo -e "${YELLOW}Dashboard API did not start (see message above); continuing the build without it.${NC}"
    fi

    # Phase F (v7.5.23): cross-project context discovery. Walks ONE parent
    # level looking for sibling repos sharing a `.loki/app.json` marker.
    # Exports LOKI_PROJECT_GRAPH_{ROOT,APP_ID,MEMBERS} which survive exec
    # into run.sh. Silent no-op when no graph is found (backward compat).
    local _pg_helper="$SKILL_DIR/autonomy/lib/project-graph.sh"
    if [ -f "$_pg_helper" ]; then
        # shellcheck disable=SC1090
        . "$_pg_helper"
        local _pg_target="${LOKI_TARGET_DIR:-$(pwd)}"
        loki_project_graph_discover "$_pg_target" || true
    fi

    # detach-to-background by default on an interactive TTY. Fires a rich,
    # reversible handoff card, then arms the existing --bg path so run.sh
    # backgrounds the runner (it writes the log + PID file and prints its own
    # "Running in Background" banner). The HARD gate below keeps every
    # non-interactive / --bg / --yes / CI caller on the exact pre-existing
    # foreground exec -- byte-identical to before this change.
    local _bg_already=""
    for _a in ${args[@]+"${args[@]}"}; do
        if [ "$_a" = "--bg" ]; then _bg_already=1; break; fi
    done
    if _loki_start_should_handoff "$_bg_already"; then
        local _spec_label="${prd_file:-codebase (no PRD)}"
        local _tier_label="${LOKI_COMPLEXITY:-auto}"
        local _decision
        _decision="$(_loki_start_handoff "$_spec_label" "$_tier_label" "$effective_provider")"
        # The new handoff already rendered the Autonomi brand banner, so tell
        # run.sh to skip its legacy ASCII "LOKI MODE" banner (avoids a duplicate,
        # redundant second banner). The legacy banner still shows on the
        # non-handoff path (CI / --bg / --yes / non-interactive), unchanged.
        [ "$_decision" != "cancel" ] && export LOKI_BRAND_SHOWN=1
        case "$_decision" in
            cancel)
                exit 0
                ;;
            launch-bg-dashboard|launch-bg)
                # Both (default) AND Dashboard open the browser -- Both = dashboard
                # + cockpit, honoring the card's "open dashboard" promise. The
                # cockpit hint is printed by _loki_start_handoff for the Both path.
                args+=("--bg")
                # The browser UI is the Control Plane; open it (guarded by
                # loki_open_url: no-op headless and in tests).
                ( LOKI_UI_NO_CLASSIC=1 cmd_ui 2>/dev/null || true ) &
                ;;
            launch-bg-watch)
                # Watch (cockpit only) detaches without opening the browser.
                args+=("--bg")
                ;;
        esac
    fi

    # v7.7.13 fix (user-reported bug): `"${args[@]}"` triggers "unbound
    # variable" under bash 3.2 (macOS default) + `set -u` when args is empty.
    # User report: `loki start` with no PRD on /Users/lokesh/git/anonima
    # crashed at this line. Safe expansion guards against empty arrays.
    _loki_new_session_exec "$RUN_SH" ${args[@]+"${args[@]}"}
}

# Check if session is running
# Usage: is_session_running [session_id]
# Without args: returns true if ANY session (global or per-session) is running
# With session_id: returns true only if that specific session is running
is_session_running() {
    local target_session="${1:-}"

    if [ -n "$target_session" ]; then
        # Check specific session
        local session_pid_file="$LOKI_DIR/sessions/$target_session/loki.pid"
        if [ -f "$session_pid_file" ]; then
            local pid
            pid=$(cat "$session_pid_file" 2>/dev/null)
            if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                return 0
            fi
        fi
        # Also check legacy per-run PID file (run-<number>.pid)
        local run_pid_file="$LOKI_DIR/run-${target_session}.pid"
        if [ -f "$run_pid_file" ]; then
            local pid
            pid=$(cat "$run_pid_file" 2>/dev/null)
            if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                return 0
            fi
        fi
        return 1
    fi

    # Check global PID files (loki.pid from run.sh, run.pid legacy)
    local pid_files=("$LOKI_DIR/loki.pid" "$LOKI_DIR/run.pid")
    for pid_file in "${pid_files[@]}"; do
        if [ -f "$pid_file" ]; then
            local pid
            pid=$(cat "$pid_file" 2>/dev/null)
            if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                return 0
            fi
        fi
    done

    # Check per-session PIDs
    if [ -d "$LOKI_DIR/sessions" ]; then
        for session_dir in "$LOKI_DIR/sessions"/*/; do
            [ -d "$session_dir" ] || continue
            local pid_file="${session_dir}loki.pid"
            if [ -f "$pid_file" ]; then
                local pid
                pid=$(cat "$pid_file" 2>/dev/null)
                if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                    return 0
                fi
            fi
        done
    fi
    return 1
}

# List all running session IDs
list_running_sessions() {
    local sessions=()
    # Check global session
    if [ -f "$LOKI_DIR/loki.pid" ]; then
        local pid
        pid=$(cat "$LOKI_DIR/loki.pid" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            sessions+=("global:$pid")
        fi
    fi
    # Check per-session PIDs
    if [ -d "$LOKI_DIR/sessions" ]; then
        for session_dir in "$LOKI_DIR/sessions"/*/; do
            [ -d "$session_dir" ] || continue
            local sid pid_file pid
            sid=$(basename "$session_dir")
            pid_file="${session_dir}loki.pid"
            if [ -f "$pid_file" ]; then
                pid=$(cat "$pid_file" 2>/dev/null)
                if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                    sessions+=("$sid:$pid")
                fi
            fi
        done
    fi
    # Check legacy run-*.pid files
    for run_pid_file in "$LOKI_DIR"/run-*.pid; do
        [ -f "$run_pid_file" ] || continue
        local sid pid
        sid=$(basename "$run_pid_file" .pid)
        sid="${sid#run-}"
        pid=$(cat "$run_pid_file" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            # Avoid duplicates (if also in sessions/)
            local dup=false
            for s in ${sessions[@]+"${sessions[@]}"}; do
                if [[ "$s" == "$sid:"* ]]; then dup=true; break; fi
            done
            if ! $dup; then
                sessions+=("$sid:$pid")
            fi
        fi
    done
    printf '%s\n' ${sessions[@]+"${sessions[@]}"}
}

# Kill a PID with SIGTERM, wait, then SIGKILL if needed
_kill_pid() {
    local pid="$1"
    pkill -P "$pid" 2>/dev/null || true
    kill "$pid" 2>/dev/null || true
    sleep 1
    if kill -0 "$pid" 2>/dev/null; then
        pkill -9 -P "$pid" 2>/dev/null || true
        kill -9 "$pid" 2>/dev/null || true
    fi
}

# v7.7.34 group-kill, factored (loki-stop-F1). Reaps the orchestrator's whole
# process group via the recorded pgid so the autonomous agent (claude/codex/
# aider), which shares the orchestrator group and would otherwise reparent to
# init and keep editing files, is killed atomically. This is the SAME logic the
# no-arg `loki stop` path used inline; it is now shared so the by-id path
# (`loki stop <session-id>`) gets identical reaping instead of skipping it.
#
# Args: one or more pgid-file paths. Each is read, validated (numeric, > 1, NOT
# this shell's own group), and group-killed; the file is removed after. A
# protected-pid conflict (dashboard / app-runner / registered pids that happen
# to share the group) downgrades the kill to a per-pid kill that excludes the
# protected pids, so a group-kill never tears down the dashboard. Every kill is
# `|| true` guarded -- safe under set -e (killing an already-dead member, or an
# empty group, returns non-zero legitimately).
_stop_group_by_pgid_files() {
    local _stop_pgid_file
    for _stop_pgid_file in "$@"; do
        [ -f "$_stop_pgid_file" ] || continue
        local _spgid
        _spgid=$(cat "$_stop_pgid_file" 2>/dev/null | tr -d ' ')
        case "$_spgid" in ''|*[!0-9]*) continue ;; esac
        [ "$_spgid" -gt 1 ] 2>/dev/null || continue
        local _my_pgid
        _my_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
        [ "$_spgid" = "$_my_pgid" ] && continue   # never kill our own group
        # Collect protected pids (dashboard, app-runner, registered pids) so
        # a group-kill never takes down the dashboard if it happens to share
        # the orchestrator group. Mirrors the dashboard Python route.
        local _protected=" "
        local _pf
        if [ -d "$LOKI_DIR/pids" ]; then
            for _pf in "$LOKI_DIR/pids"/*.json; do
                [ -f "$_pf" ] || continue
                _protected="${_protected}$(basename "$_pf" .json) "
            done
        fi
        for _pf in "$LOKI_DIR/dashboard/dashboard.pid" "${HOME}/.loki/dashboard/dashboard.pid"; do
            [ -f "$_pf" ] && _protected="${_protected}$(cat "$_pf" 2>/dev/null | tr -d ' ') "
        done
        # Does any protected pid share this group?
        local _conflict=0 _gp
        for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
            case "$_protected" in *" $_gp "*) _conflict=1; break ;; esac
        done
        if [ "$_conflict" = "1" ]; then
            # Per-pid kill of group members EXCLUDING protected pids.
            for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
                case "$_protected" in *" $_gp "*) continue ;; esac
                [ "$_gp" = "$$" ] && continue
                kill -TERM "$_gp" 2>/dev/null || true
            done
            sleep 1
            for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
                case "$_protected" in *" $_gp "*) continue ;; esac
                [ "$_gp" = "$$" ] && continue
                kill -KILL "$_gp" 2>/dev/null || true
            done
        else
            kill -TERM -- -"$_spgid" 2>/dev/null || true
            sleep 1
            kill -KILL -- -"$_spgid" 2>/dev/null || true
        fi
        rm -f "$_stop_pgid_file" 2>/dev/null || true
    done
}

# Stop a specific session by its session ID
_stop_session_by_id() {
    local sid="$1"
    local session_dir="$LOKI_DIR/sessions/$sid"
    local pid_file="$session_dir/loki.pid"

    if [ -f "$pid_file" ]; then
        local pid
        pid=$(cat "$pid_file" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            _kill_pid "$pid"
            echo -e "${RED}Stopped session $sid (PID: $pid)${NC}"
        fi
        rm -f "$pid_file"
    fi

    # Also check legacy run-<id>.pid
    local run_pid_file="$LOKI_DIR/run-${sid}.pid"
    if [ -f "$run_pid_file" ]; then
        local pid
        pid=$(cat "$run_pid_file" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            _kill_pid "$pid"
        fi
        rm -f "$run_pid_file"
    fi

    # Clean up session lock file
    rm -f "$session_dir/session.lock" 2>/dev/null
}

# Stop a specific session or all sessions
# Usage: loki stop [session_id]
# Without args: stops all running sessions
# With session_id: stops only that specific session
cmd_stop() {
    local target_session=""
    local stop_all=false

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki stop${NC} - Stop running sessions"
                echo ""
                echo "Usage: loki stop [session-id] [--all]"
                echo ""
                echo "  loki stop          Stop ONLY the current folder's session."
                echo "                     Other folders keep running. Marks this"
                echo "                     project stopped in the dashboard registry."
                echo "  loki stop 52       Stop only session #52 in this folder."
                echo "  loki stop --all    Stop EVERY loki runner on this machine"
                echo "                     (legacy machine-wide behavior)."
                echo ""
                echo "The shared dashboard stays up if other projects are still"
                echo "running; it is stopped only when no project remains (or --all)."
                echo ""
                echo "Use 'loki status' to see this folder's sessions."
                exit 0
                ;;
            --all)
                stop_all=true
                shift
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki stop --help' for usage."
                exit 1
                ;;
            *)
                target_session="$1"
                shift
                ;;
        esac
    done

    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${YELLOW}No .loki directory found.${NC}"
        echo "No active session to stop."
        emit_learning_signal error_pattern \
            --source cli \
            --action "cmd_stop" \
            --error-type "SessionNotFound" \
            --error-message "No .loki directory found" \
            --recovery-steps '["Run loki start first"]' \
            --confidence 0.7
        exit 0
    fi

    # Stop a specific session by ID
    if [ -n "$target_session" ]; then
        if is_session_running "$target_session"; then
            # loki-stop-F1: group-kill FIRST (v7.7.34 discipline), scoped to THIS
            # session's recorded pgid. Without this the by-id path reaped only the
            # orchestrator pid (via _stop_session_by_id -> _kill_pid), leaving the
            # autonomous agent (claude/codex/aider) -- which shares the
            # orchestrator's process group -- to reparent to init and keep editing
            # files. That is exactly the v7.7.34 orphaned-agent bug, reopened on
            # the by-id path. The pgid is session-scoped: run.sh writes it next to
            # the session pid as ${pid_file%.pid}.pgid, so we only ever touch THIS
            # session's group (modern sessions/<id>/loki.pgid + legacy
            # run-<id>.pgid), never a sibling session or another folder.
            #
            # Deliberately NOT mirrored from the no-arg path: the docker reap,
            # session.json->stopped, and dashboard registry mark are folder/global
            # side effects (the docker container is named by workspace sha with no
            # per-session container; registry.mark_project_stopped marks the whole
            # project; session.json is the folder-level skill session). Firing them
            # on a by-id stop would mismark the project / kill a docker run while a
            # sibling session in the same folder is still building. The group-kill
            # alone closes the stated orphaned-agent hole; folder-global teardown
            # stays on the no-arg / --all paths.
            _stop_group_by_pgid_files \
                "$LOKI_DIR/sessions/$target_session/loki.pgid" \
                "$LOKI_DIR/run-${target_session}.pgid"
            _stop_session_by_id "$target_session"
            echo "Stopped session: $target_session"
        else
            echo "No active session found"
        fi
        return 0
    fi

    # FIX-DOCKER-STOP (reap side): a `loki docker start` in this folder runs the
    # build inside a container named loki-<sha12 of workspace>; the host .loki has
    # no live pid for it, so the legacy "No active session" path would fire while
    # the container is Up. Reconcile + reap it here, before the session check.
    # Folder-scoped: the container name derives from THIS folder's run.json (or a
    # recompute over $(pwd)), so a run in another folder is never touched (AC18).
    local _docker_reaped=0
    if command -v docker >/dev/null 2>&1; then
        local _docker_runstate="$LOKI_DIR/docker/run.json"
        local _docker_container=""
        if [ -f "$_docker_runstate" ] && command -v python3 >/dev/null 2>&1; then
            _docker_container=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('container',''))" "$_docker_runstate" 2>/dev/null || true)
        fi
        # Fallback: recompute the deterministic name via Agent B's helper. cmd_stop
        # does NOT source docker-run.sh itself (only cmd_docker does), so source it
        # here, gated on the module existing, to make the helper available.
        if [ -z "$_docker_container" ]; then
            local _docker_mod="$_LOKI_SCRIPT_DIR/docker-run.sh"
            if [ -f "$_docker_mod" ]; then
                # shellcheck source=/dev/null
                source "$_docker_mod" 2>/dev/null || true
                if declare -F loki_docker_container_name >/dev/null 2>&1; then
                    _docker_container="$(loki_docker_container_name 2>/dev/null || true)"
                fi
            fi
        fi
        if [ -n "$_docker_container" ]; then
            # Only reap if a container with exactly this name is actually running.
            local _running_id
            _running_id=$(docker ps -q -f "name=^${_docker_container}$" 2>/dev/null || true)
            if [ -n "$_running_id" ]; then
                docker stop "$_docker_container" >/dev/null 2>&1 || true
                # --rm may auto-remove on stop; rm is best-effort (already-gone is fine).
                docker rm "$_docker_container" >/dev/null 2>&1 || true
                _docker_reaped=1
                echo -e "${RED}Stopped docker run: ${_docker_container}${NC}"
            fi
        fi
        # Clear our run-state record regardless (the container is gone or absent).
        rm -f "$_docker_runstate" 2>/dev/null || true
    fi

    # No session ID given -- stop all running sessions
    if is_session_running; then
        # Stop per-session PIDs first
        if [ -d "$LOKI_DIR/sessions" ]; then
            for session_dir in "$LOKI_DIR/sessions"/*/; do
                [ -d "$session_dir" ] || continue
                local sid
                sid=$(basename "$session_dir")
                if is_session_running "$sid"; then
                    _stop_session_by_id "$sid"
                fi
            done
        fi
        # Stop legacy run-*.pid sessions
        for run_pid_file in "$LOKI_DIR"/run-*.pid; do
            [ -f "$run_pid_file" ] || continue
            local pid
            pid=$(cat "$run_pid_file" 2>/dev/null)
            if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                _kill_pid "$pid"
                local sid
                sid=$(basename "$run_pid_file" .pid)
                echo -e "${RED}Stopped session ${sid#run-} (PID: $pid)${NC}"
            fi
            rm -f "$run_pid_file"
        done

        # Stop global session
        touch "$LOKI_DIR/STOP"

        # v7.7.34: group-kill first. The autonomous agent (claude/codex/aider)
        # shares the orchestrator's process group (the runner is launched as a
        # session leader), so signaling the whole group reaps the orchestrator
        # AND the agent child atomically. Killing only the orchestrator pid lets
        # the agent reparent to init and keep editing files -- the reported bug.
        # Factored into _stop_group_by_pgid_files (loki-stop-F1) so the by-id stop
        # path performs identical reaping. Here we pass the GLOBAL pgid files.
        _stop_group_by_pgid_files "$LOKI_DIR/loki.pgid" "$LOKI_DIR/run.pgid"

        local killed_pid=""
        for pid_file in "$LOKI_DIR/loki.pid" "$LOKI_DIR/run.pid"; do
            if [ -f "$pid_file" ]; then
                local pid
                pid=$(cat "$pid_file" 2>/dev/null)
                if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                    _kill_pid "$pid"
                    killed_pid="$pid"
                fi
                rm -f "$pid_file"
            fi
        done

        # Mark session.json as stopped (skill-invoked sessions)
        # BUG-ST-008: Atomic session.json update via temp file + mv (matches run.sh)
        if [ -f "$LOKI_DIR/session.json" ]; then
            _LOKI_SESSION_FILE="$LOKI_DIR/session.json" python3 -c "
import json, os, tempfile
sf = os.environ['_LOKI_SESSION_FILE']
try:
    with open(sf) as f:
        d = json.load(f)
    d['status'] = 'stopped'
    sd = os.path.dirname(sf)
    fd, tmp = tempfile.mkstemp(dir=sd, suffix='.json')
    with os.fdopen(fd, 'w') as f:
        json.dump(d, f)
    os.replace(tmp, sf)
except (json.JSONDecodeError, OSError): pass
" 2>/dev/null || true
        fi

        # v7.7.30: mark THIS project stopped in the shared dashboard registry
        # so the multi-project switcher reflects it. Best-effort, never fatal.
        # Honors LOKI_SKIP_PROJECT_REGISTRY (matches run.sh registration).
        if [ -z "${LOKI_SKIP_PROJECT_REGISTRY:-}" ] && command -v python3 >/dev/null 2>&1; then
            LOKI_REG_TARGET="$(pwd)" LOKI_REG_SKILL="${SKILL_DIR:-$PROJECT_DIR}" \
            python3 - <<'PYSTOP' >/dev/null 2>&1 || true
import os, sys
sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
try:
    from dashboard import registry
    registry.mark_project_stopped(os.path.abspath(os.environ["LOKI_REG_TARGET"]))
except Exception:
    pass
PYSTOP
        fi

        # Clean up control files
        rm -f "$LOKI_DIR/STOP" "$LOKI_DIR/PAUSE" "$LOKI_DIR/PAUSED.md" 2>/dev/null

        # Kill dashboard if running.
        # v7.7.30: the project-local dashboard (.loki/dashboard) belongs solely
        # to this folder and is ALWAYS killed. The shared standalone dashboard
        # (~/.loki/dashboard) is killed ONLY when no OTHER registered project is
        # still running, so we never tear down a dashboard other folders need.
        # For --all, the shared dashboard is always killed (tear everything down).
        local _kill_shared_dash=false
        if [ "$stop_all" = true ]; then
            _kill_shared_dash=true
        elif command -v python3 >/dev/null 2>&1; then
            # Query the registry (after mark_project_stopped excluded this
            # project) for any OTHER project whose pid is still alive.
            local _dash_decision
            _dash_decision=$(LOKI_REG_SKILL="${SKILL_DIR:-$PROJECT_DIR}" \
                python3 - <<'PYDASH' 2>/dev/null || true
import os, sys
sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
try:
    from dashboard import registry
    alive = 0
    for p in registry.list_projects(include_inactive=True):
        pid = p.get("pid")
        if isinstance(pid, int) and pid > 0:
            try:
                os.kill(pid, 0)
                alive += 1
            except OSError:
                pass
    print("CLEAR" if alive == 0 else "KEEP")
except Exception:
    print("CLEAR")
PYDASH
            )
            [ "$_dash_decision" = "CLEAR" ] && _kill_shared_dash=true
        else
            # python3 unavailable: fall back to legacy behavior (kill shared)
            # to avoid leaking the shared dashboard on minimal systems.
            _kill_shared_dash=true
        fi

        local _dash_pidf
        local _dash_targets=("$LOKI_DIR/dashboard/dashboard.pid")
        [ "$_kill_shared_dash" = true ] && _dash_targets+=("${HOME}/.loki/dashboard/dashboard.pid")
        for _dash_pidf in "${_dash_targets[@]}"; do
            if [ -f "$_dash_pidf" ]; then
                local dash_pid
                dash_pid=$(cat "$_dash_pidf" 2>/dev/null)
                if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
                    kill "$dash_pid" 2>/dev/null || true
                    sleep 0.5
                    kill -9 "$dash_pid" 2>/dev/null || true
                fi
                rm -f "$_dash_pidf"
            fi
        done

        # Kill any remaining registered processes (2s graceful window matches run.sh)
        if [ -d "$LOKI_DIR/pids" ]; then
            for entry_file in "$LOKI_DIR/pids"/*.json; do
                [ -f "$entry_file" ] || continue
                local reg_pid
                reg_pid=$(basename "$entry_file" .json)
                case "$reg_pid" in ''|*[!0-9]*) continue ;; esac
                if kill -0 "$reg_pid" 2>/dev/null; then
                    kill "$reg_pid" 2>/dev/null || true
                    local w=0
                    while [ $w -lt 4 ] && kill -0 "$reg_pid" 2>/dev/null; do
                        sleep 0.5
                        w=$((w + 1))
                    done
                    if kill -0 "$reg_pid" 2>/dev/null; then
                        kill -9 "$reg_pid" 2>/dev/null || true
                    fi
                fi
                rm -f "$entry_file"
            done
        fi

        # Emit session stop event
        emit_event session cli stop "reason=user_requested"
        # Emit success pattern for clean stop (SYN-018)
        emit_learning_signal success_pattern \
            --source cli \
            --action "cmd_stop" \
            --pattern-name "session_stop" \
            --action-sequence '["check_session", "send_stop_signal"]' \
            --outcome success \
            --confidence 0.9

        if [ -n "$killed_pid" ]; then
            echo -e "${RED}Stopped Loki Mode (PID: $killed_pid)${NC}"
        else
            echo -e "${RED}STOP signal sent. Execution will halt immediately.${NC}"
        fi
    elif [ "$_docker_reaped" != "1" ]; then
        echo -e "${YELLOW}No active session running.${NC}"
        if [ -f "$LOKI_DIR/STOP" ]; then
            echo "STOP signal already present."
        else
            echo "Start a session with: loki start"
        fi
    fi

    # v7.7.30: --all preserves the legacy machine-wide kill. It runs even when
    # the current folder has no live session (the "clean everything" use case),
    # reaping every folder's orphaned loki-run-* temp script (SIGTERM, SIGKILL).
    #
    # The kill pattern is "loki-run-" for real users and is NOT user-overridable
    # at the CLI -- that is the documented machine-wide behavior. LOKI_STOP_ALL_PATTERN
    # exists ONLY so the test suite can exercise this exact code path against its
    # own uniquely-marked fake runners without SIGKILLing an unrelated live
    # loki-run-* on the same machine (e.g. a long SWE-bench instance). It defaults
    # to "loki-run-" so user-facing semantics are unchanged when unset.
    if [ "$stop_all" = true ]; then
        local _stop_all_pat="${LOKI_STOP_ALL_PATTERN:-loki-run-}"
        pkill -f "$_stop_all_pat" 2>/dev/null || true
        sleep 0.5
        pkill -9 -f "$_stop_all_pat" 2>/dev/null || true
        echo -e "${RED}--all: signalled all loki-run-* processes on this machine.${NC}"

        # FIX-DOCKER-STOP --all: also reap EVERY loki-mode container on this
        # machine (parity with the machine-wide PID kill above). Best-effort.
        if command -v docker >/dev/null 2>&1; then
            local _all_ids
            _all_ids=$(docker ps -q --filter ancestor=asklokesh/loki-mode 2>/dev/null || true)
            if [ -n "$_all_ids" ]; then
                # shellcheck disable=SC2086
                docker stop $_all_ids >/dev/null 2>&1 || true
                # shellcheck disable=SC2086
                docker rm $_all_ids >/dev/null 2>&1 || true
                echo -e "${RED}--all: stopped all loki-mode docker containers on this machine.${NC}"
            fi
        fi
    fi
}

# Classify one current-project PID registry entry without signalling anything.
#
# A numeric PID alone is not process identity because the OS reuses PIDs. The registry's
# UTC `started` field is the available launch token: the live process must have started
# within a bounded window AROUND the recorded registration, and that registration must
# itself be corroborated by the entry's st_ctime, which unprivileged userspace cannot
# backdate. The file and its parent must also remain current-UID-owned, non-linked,
# canonical local state. Any missing validation fails closed.
#
# Prints exactly one of: OWNED, STALE, REFUSED, UNAVAILABLE.
_cleanup_registry_entry_state() {
    local entry_file="$1" expected_pid="$2"
    command -v python3 >/dev/null 2>&1 || {
        printf '%s\n' "UNAVAILABLE"
        return 0
    }
    python3 - "$entry_file" "$expected_pid" <<'PY' 2>/dev/null || printf '%s\n' "REFUSED"
import calendar
import json
import os
from pathlib import Path
import stat
import subprocess
import sys
import time


def verdict(value: str) -> None:
    print(value)
    raise SystemExit(0)


try:
    entry = Path(sys.argv[1])
    expected_pid = int(sys.argv[2])
    parent = entry.parent
    parent_info = os.lstat(parent)
    entry_info = os.lstat(entry)
    if (
        expected_pid <= 1
        or entry.name != f"{expected_pid}.json"
        or not stat.S_ISDIR(parent_info.st_mode)
        or stat.S_ISLNK(parent_info.st_mode)
        or parent_info.st_uid != os.getuid()
        or stat.S_IMODE(parent_info.st_mode) & 0o022
        or os.path.realpath(parent) != os.path.abspath(parent)
        or not stat.S_ISREG(entry_info.st_mode)
        or stat.S_ISLNK(entry_info.st_mode)
        or entry_info.st_uid != os.getuid()
        or entry_info.st_nlink != 1
        or stat.S_IMODE(entry_info.st_mode) & 0o022
    ):
        verdict("REFUSED")

    record = json.loads(entry.read_text(encoding="utf-8"))
    pid = record.get("pid")
    ppid = record.get("ppid")
    label = record.get("label")
    started = record.get("started")
    if (
        isinstance(pid, bool)
        or not isinstance(pid, int)
        or pid != expected_pid
        or isinstance(ppid, bool)
        or not isinstance(ppid, int)
        or ppid <= 0
        or not isinstance(label, str)
        or not label
        or not isinstance(started, str)
    ):
        verdict("REFUSED")

    try:
        os.kill(pid, 0)
    except ProcessLookupError:
        verdict("STALE")
    except (PermissionError, OSError):
        verdict("REFUSED")

    # register_pid() (autonomy/run.sh) writes `started` on the line right after the
    # `&` that spawned the pid, so for a genuine child entry the process launch and
    # the recorded registration are seconds apart in BOTH directions.
    #
    # `kind":"wrapper"` entries are NOT this shape: register_self_wrapper() records
    # the wrapper's OWN pid deep in main() startup, so the honest gap is unbounded
    # and no tight window fits. They are also not cmd_cleanup's to reap -- run.sh's
    # cleanup_orphan_pids owns them behind a parent-dead AND idle AND no-live-child
    # predicate, because cmd_cleanup's parent-death-only rule is wrong for a
    # wrapper. Refuse them here rather than widen the window for every entry: a
    # window wide enough for a wrapper is wide enough to forge a child.
    if record.get("kind") is not None:
        verdict("REFUSED")

    SLACK = 10
    registered_at = calendar.timegm(time.strptime(started, "%Y-%m-%dT%H:%M:%SZ"))
    if registered_at > time.time() + 5:
        verdict("REFUSED")

    # `started` is attacker-writable, so it cannot vouch for itself. st_ctime is
    # the one field in this registry that unprivileged userspace cannot backdate
    # (utimes moves atime/mtime only), so it pins WHEN the entry was really made.
    # Without this an attacker just backdates `started` to an old victim's real
    # launch time and satisfies the window below.
    if abs(entry_info.st_ctime - registered_at) > SLACK:
        verdict("REFUSED")

    observed = subprocess.run(
        ["ps", "-o", "uid=", "-o", "ppid=", "-o", "lstart=", "-p", str(pid)],
        check=False,
        capture_output=True,
        text=True,
        timeout=2,
    )
    # lstart contains spaces, so split off uid and ppid only; the remainder is lstart.
    fields = observed.stdout.strip().split(maxsplit=2)
    if observed.returncode != 0 or len(fields) != 3 or int(fields[0]) != os.getuid():
        verdict("REFUSED")

    # The recorded `ppid` is not decoration: register_pid() (autonomy/run.sh) writes
    # `"ppid":$$`, so for a genuine entry the registering shell IS the live process's
    # real parent. Nothing above binds the entry to that parentage, which is the whole
    # forgery: a fresh, same-uid, correctly-ctime'd entry naming an UNRELATED live pid
    # with an arbitrary dead `ppid` satisfies every other check and gets TERM then KILL.
    #
    # Two honest shapes, and only two:
    #   recorded parent alive -> it must still BE the live parent.
    #   recorded parent dead  -> the child was reparented to init (pid 1), which is the
    #                            genuine orphan case cmd_cleanup exists to reap.
    # A forged entry aimed at an unrelated process fails both: that process's real
    # parent is the attacker's own live shell, which is neither the recorded ppid nor 1.
    # Both ceilings below fail CLOSED (a genuine entry is skipped and left for
    # inspection, never an unrelated process signalled), so neither can turn into
    # the forgery this guard exists to stop:
    #   - if the recorded ppid died and the OS reused that number for an unrelated
    #     live process, this reads "parent alive" and refuses a real orphan;
    #   - on Linux under a PR_SET_CHILD_SUBREAPER ancestor (systemd user scope) an
    #     orphan reparents to the subreaper rather than to 1 and is refused.
    # Widening either one means accepting an arbitrary live pid as the parent, which
    # is exactly the hole above, so the strict form stays until a portable way to
    # identify the real reaper exists. Reparent-to-init matches what the rest of the
    # runtime already assumes (see run.sh's detached getppid()==1 note).
    observed_ppid = int(fields[1])
    recorded_parent_alive = True
    try:
        os.kill(ppid, 0)
    except ProcessLookupError:
        recorded_parent_alive = False
    except (PermissionError, OSError):
        verdict("REFUSED")
    if observed_ppid != (ppid if recorded_parent_alive else 1):
        verdict("REFUSED")

    process_started_at = int(time.mktime(time.strptime(fields[2], "%a %b %d %H:%M:%S %Y")))
    # Upper bound rejects a pid reused AFTER registration. Lower bound rejects a
    # forged entry aimed at an unrelated process that was already running when
    # the entry was written. Both halves are required: with only the upper bound
    # any live same-uid pid older than `started` is signalable.
    if not (registered_at - SLACK <= process_started_at <= registered_at + 1):
        verdict("REFUSED")
except (OSError, ValueError, TypeError, json.JSONDecodeError, subprocess.SubprocessError):
    verdict("REFUSED")

verdict("OWNED")
PY
}

# Kill orphaned processes from crashed sessions
cmd_cleanup() {
    # v7.6.2 B-13 fix: --help must print help, not run cleanup as a side effect.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- cleanup orphaned processes${NC}"
            echo ""
            echo "Usage: loki cleanup [options]"
            echo ""
            echo "Scans \$LOKI_DIR/pids/ for stale PID registrations from prior runs"
            echo "and kills processes whose PID file points at a dead process."
            echo ""
            echo "Options:"
            echo "  --help, -h   Show this help and exit"
            echo ""
            echo "Side effects: kills processes registered under .loki/pids/."
            return 0
            ;;
    esac
    local pids_dir="$LOKI_DIR/pids"
    local killed=0
    local stale=0
    local refused=0

    if [ ! -d "$pids_dir" ]; then
        echo "No PID registry found. Nothing to clean up."
        exit 0
    fi

    echo -e "${BOLD}Scanning for orphaned processes...${NC}"

    for entry_file in "$pids_dir"/*.json; do
        [ -f "$entry_file" ] || continue
        local pid
        pid=$(basename "$entry_file" .json)
        case "$pid" in
            ''|*[!0-9]*) continue ;;
        esac

        local registry_state
        registry_state=$(_cleanup_registry_entry_state "$entry_file" "$pid")
        case "$registry_state" in
            STALE)
                rm -f "$entry_file"
                stale=$((stale + 1))
                continue
                ;;
            OWNED) ;;
            UNAVAILABLE)
                echo -e "  ${YELLOW}Refusing${NC} PID=$pid (python3 unavailable: ownership cannot be verified)"
                refused=$((refused + 1))
                continue
                ;;
            *)
                echo -e "  ${YELLOW}Refusing${NC} PID=$pid (registry ownership or launch identity is not valid)"
                refused=$((refused + 1))
                continue
                ;;
        esac

        local label=""
        local ppid_val=""
        # Parse JSON fields (python3 with shell fallback)
        if command -v python3 >/dev/null 2>&1; then
            label=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('label','unknown'))" "$entry_file" 2>/dev/null) || label="unknown"
            ppid_val=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('ppid',''))" "$entry_file" 2>/dev/null) || true
        else
            label=$(sed 's/.*"label":"//' "$entry_file" 2>/dev/null | sed 's/".*//' | head -1) || label="unknown"
            ppid_val=$(sed 's/.*"ppid"://' "$entry_file" 2>/dev/null | sed 's/[,}].*//' | head -1) || true
        fi

        # The ownership gate above proved this exact live process matches the
        # current-project registry launch token. Check only whether its exact
        # recorded parent is gone before normal orphan cleanup.
        local is_orphan=false
        case "$ppid_val" in ''|*[!0-9]*) ppid_val="" ;; esac
        if [ -n "$ppid_val" ] && ! kill -0 "$ppid_val" 2>/dev/null; then
            is_orphan=true
        fi

        if [ "$is_orphan" = true ] || [ "${1:-}" = "--force" ]; then
            # Close the validation-to-signal window as far as this portable PID
            # registry permits. Re-check again after TERM before any KILL so a
            # rapidly reused PID is never escalated under the old identity.
            registry_state=$(_cleanup_registry_entry_state "$entry_file" "$pid")
            if [ "$registry_state" != "OWNED" ]; then
                echo -e "  ${YELLOW}Refusing${NC} PID=$pid (launch identity changed before signalling)"
                refused=$((refused + 1))
                continue
            fi
            echo -e "  ${RED}Killing${NC} PID=$pid label=$label (parent $ppid_val dead)"
            if ! kill "$pid" 2>/dev/null; then
                echo -e "  ${YELLOW}Refusing${NC} PID=$pid (TERM could not be delivered)"
                refused=$((refused + 1))
                continue
            fi
            sleep 0.5
            registry_state=$(_cleanup_registry_entry_state "$entry_file" "$pid")
            case "$registry_state" in
                STALE)
                    rm -f "$entry_file"
                    killed=$((killed + 1))
                    ;;
                OWNED)
                    if kill -9 "$pid" 2>/dev/null; then
                        rm -f "$entry_file"
                        killed=$((killed + 1))
                    else
                        echo -e "  ${YELLOW}Refusing${NC} PID=$pid (KILL could not be delivered)"
                        refused=$((refused + 1))
                    fi
                    ;;
                *)
                    echo -e "  ${YELLOW}Refusing${NC} PID=$pid (launch identity changed before escalation)"
                    refused=$((refused + 1))
                    ;;
            esac
        else
            echo -e "  ${GREEN}Alive${NC}  PID=$pid label=$label (parent $ppid_val alive)"
        fi
    done

    echo ""
    echo "Results: $killed orphan(s) killed, $stale stale entries cleaned, $refused entry(s) refused"

    if [ $killed -eq 0 ] && [ $stale -eq 0 ] && [ $refused -eq 0 ]; then
        echo -e "${GREEN}System is clean. No orphans found.${NC}"
    fi
}

# P12-10b: Loki 10 run control. True when the newest engine10 run has no run.completed yet. A pending request
# ("$1" = pause) additionally requires control-request.json to hold that action.
_loki_e10_active_run() {
    local last="" d
    for d in "$LOKI_DIR"/runs/e10-*; do [ -d "$d" ] && last="$d"; done
    [ -n "$last" ] && [ -f "$last/events.jsonl" ] || return 1
    ! grep -q '"type":"run.completed"' "$last/events.jsonl" 2>/dev/null || return 1
    [ -z "${1:-}" ] || grep -q "\"action\":\"$1\"" "$last/control-request.json" 2>/dev/null
}

# loki cancel: asks the active Loki 10 run to stop at its next stage boundary; the run ends CANCELLED with no PR.
cmd_cancel() {
    case "${1:-}" in
        --help|-h)
            echo -e "${BOLD}Loki Mode -- cancel the active Loki 10 run${NC}"
            echo ""
            echo "Usage: loki cancel"
            echo ""
            echo "Writes a cancel request that the engine reads at the next stage boundary."
            echo "The run then completes as CANCELLED (run.completed with cancelled:true)."
            echo ""
            echo "See also: loki pause, loki resume, loki stop"
            return 0
            ;;
    esac
    if _loki_e10_active_run; then
        ( LOKI_DIR="$LOKI_DIR" _loki_bun_delegate cancel ) || {
            echo "loki cancel needs the Bun engine, which is unavailable: ${_LOKI_DELEGATE_WHY:-bun route unavailable}" >&2
            return 1
        }
        return 0
    fi
    echo -e "${YELLOW}No active Loki 10 run to cancel.${NC}"
    return 1
}

# Pause after current session
cmd_pause() {
    # v7.6.2 B-13 fix: --help must print help, not act on session state.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- pause active session${NC}"
            echo ""
            echo "Usage: loki pause [options]"
            echo ""
            echo "Marks the active session as paused. The runner detects this on"
            echo "the next iteration boundary and halts cleanly until 'loki resume'."
            echo ""
            echo "Options:"
            echo "  --help, -h   Show this help and exit"
            echo ""
            echo "See also: loki resume, loki stop"
            return 0
            ;;
    esac
    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${YELLOW}No .loki directory found.${NC}"
        echo "No active session to pause."
        exit 0
    fi

    # P12-10b: an active Loki 10 run takes a stage-boundary request; anything else keeps the legacy PAUSE file.
    if _loki_e10_active_run; then
        local _pz_rc=0
        ( LOKI_DIR="$LOKI_DIR" _loki_bun_delegate pause ) || _pz_rc=$?
        [ "$_pz_rc" -ne 0 ] || return 0
    fi

    if is_session_running; then
        # Warn if running in perpetual mode where PAUSE is auto-cleared (#84)
        local current_mode="${LOKI_AUTONOMY_MODE:-perpetual}"
        local perpetual_flag="${LOKI_PERPETUAL_MODE:-false}"
        if [ "$current_mode" = "perpetual" ] || [ "$perpetual_flag" = "true" ]; then
            echo -e "${YELLOW}Warning: Session is running in perpetual mode.${NC}"
            echo -e "${YELLOW}PAUSE signals are auto-cleared in perpetual mode and will be ignored.${NC}"
            echo -e "Use ${CYAN}loki stop${NC} to halt a perpetual session, or switch autonomy mode first:"
            echo -e "  ${DIM}loki config set autonomy_mode checkpoint${NC}"
        fi
        touch "$LOKI_DIR/PAUSE"
        # Emit session pause event
        emit_event session cli pause "reason=user_requested"
        # Emit user preference for pause action (SYN-018)
        emit_learning_signal user_preference \
            --source cli \
            --action "session_control" \
            --key "control_action" \
            --value "pause" \
            --rejected '["stop", "continue"]' \
            --confidence 0.9
        echo -e "${YELLOW}PAUSE signal sent. Execution will pause after current task.${NC}"
        echo -e "${DIM}Resume with: loki resume${NC}"
    else
        echo -e "${YELLOW}No active session running.${NC}"
        if [ -f "$LOKI_DIR/PAUSE" ]; then
            echo "PAUSE signal already present."
        else
            echo "Start a session with: loki start"
        fi
    fi
}

# RUN-25 iter 22 (Wave D #3): `loki steer "<note>"` writes an operator directive
# to .loki/HUMAN_INPUT.md. engine10 reads it before the next plan or implement
# session (default on; only LOKI_PROMPT_INJECTION=0/false opts out, steer.ts).
# The legacy loop injects it only when LOKI_PROMPT_INJECTION=true.
# Each steer is a numbered revision: `## Operator directive rev N (ts) actor=A source=S`.
# engine10 (util/steer.ts) archives the file and appends the revisions to
# .loki/intent-revisions.jsonl. Actor and source come from LOKI_STEER_ACTOR
# (default: git user.name, then $USER) and LOKI_STEER_SOURCE (cli, chat or an
# https comment URL; default cli). Both are reduced to a safe character set.
cmd_steer() {
    # "--" ends options so guidance beginning with "-" is data, not a flag
    local _eoo=0
    if [ "${1:-}" = "--" ]; then _eoo=1; shift; fi
    case "$_eoo:${1:-}" in
        1:?*) ;;
        *:--help|*:-h|*:help|*:"")
            echo -e "${BOLD}Loki Mode -- steer the running build${NC}"
            echo ""
            echo "Usage: loki steer \"<guidance>\""
            echo ""
            echo "Writes your guidance to .loki/HUMAN_INPUT.md as a numbered revision"
            echo "(rev N, actor, source). The Loki 10 engine applies it once, before the"
            echo "next plan or implement session, and records it in .loki/intent-revisions.jsonl."
            echo "A steer that arrives after the Wall (the frozen acceptance tests) was authored"
            echo "makes Seal refuse the run; rerun the task with the new intent included."
            echo ""
            echo "  loki steer \"focus on the auth flow, skip the docs for now\""
            echo ""
            echo "Opt out with LOKI_PROMPT_INJECTION=0. The legacy loop reads the note"
            echo "only when LOKI_PROMPT_INJECTION=true."
            [ -z "${1:-}" ] && return 2
            return 0
            ;;
    esac
    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${YELLOW}No .loki directory found.${NC} Start a session first: loki start"
        return 1
    fi
    local note="$*"
    local actor source rev pending archived
    actor="${LOKI_STEER_ACTOR:-$(git config user.name 2>/dev/null || true)}"
    actor="${actor:-${USER:-unknown}}"
    actor="$(printf '%s' "$actor" | tr -c 'A-Za-z0-9._@-' '-' | cut -c1-64)"
    source="$(printf '%s' "${LOKI_STEER_SOURCE:-cli}" | tr -c 'A-Za-z0-9:/._#?=&%-' '-' | cut -c1-300)"
    # rev = consumed revisions + steers still pending in the buffer + 1
    archived=0; pending=0
    [ -f "$LOKI_DIR/intent-revisions.jsonl" ] && archived="$(wc -l < "$LOKI_DIR/intent-revisions.jsonl" | tr -d ' ')"
    [ -f "$LOKI_DIR/HUMAN_INPUT.md" ] && pending="$(grep -c '^## Operator directive rev ' "$LOKI_DIR/HUMAN_INPUT.md" 2>/dev/null || true)"
    rev=$(( ${archived:-0} + ${pending:-0} + 1 ))
    # Append so multiple steers accumulate; the reader consumes and clears the
    # file, so this is the pending-directive buffer.
    {
        printf '## Operator directive rev %s (%s) actor=%s source=%s\n' "$rev" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$actor" "$source"
        printf '%s\n\n' "$note"
    } >> "$LOKI_DIR/HUMAN_INPUT.md"
    emit_event session cli steer "chars=${#note}" 2>/dev/null || true
    echo -e "${GREEN}Steering note (rev $rev) written to .loki/HUMAN_INPUT.md${NC}"
    case "${LOKI_PROMPT_INJECTION:-}" in
        0|false|FALSE|False)
            echo -e "${YELLOW}Note: LOKI_PROMPT_INJECTION is off, so the run will NOT read this.${NC}"
            ;;
        *)
            echo -e "${DIM}The Loki 10 engine applies it before the next plan or implement session.${NC}"
            # Same text the issue comment ack posts (trigger-server.py steer_ack_text).
            echo "Steer rev $rev received: applies at next plan/implement stage."
            echo "If the Wall is already frozen, Seal will refuse this run: rerun with the new intent."
            ;;
    esac
    return 0
}

# Print the one copy-pasteable command that gets an INTERRUPTED run going again,
# naming what was found (iteration reached, when it stopped) so the user can tell
# it is their run and not a stale directory. Returns 0 when a hint was printed,
# 1 when there is nothing resumable (caller then prints its own message).
#
# Keyed on status == "interrupted" ONLY, deliberately. run.sh's load_state
# preserves iterationCount for exactly paused|interrupted|budget_exceeded|
# stopped and RESETS it to 0 for everything else -- including "running", which
# is what an uncatchable SIGKILL leaves behind. Printing "resume from iteration
# 7" on a "running" record would promise progress that load_state is about to
# throw away, on a dev box without LOKI_DURABLE_STATE=1. A hint pointing at
# nothing is worse than silence, so the narrow key is the point, not an
# oversight. budget_exceeded / max_iterations_reached already get their own
# tailored guidance from _loki_next_action (raise the cap FIRST, then resume);
# duplicating them here would contradict it.
#
# Read-only: reads state, writes nothing, spends nothing.
# Print the "you can pick this up" hint for a run that STOPPED WITHOUT A VERDICT.
#
# THE DEFECT this widening fixes, reproduced: cmd_next maps
# max_iterations_reached and budget_exceeded to `loki resume`, but this helper
# accepted ONLY "interrupted", so cmd_resume fell through to "No session to
# resume. Start a session with: loki start" -- rc=0. The two commands whose
# whole job is "do the right next thing" contradicted each other on every run
# that hit a limit.
#
# WHAT MUST NOT BE ADDED HERE: council_approved, council_force_approved and
# completion_promise_fulfilled. Those carry a VERDICT and route to `loki ship`.
# Accepting them would send an approved build back into the loop.
# failed / policy_blocked / force_stopped route to `loki why` and stay out too.
# The set below is exactly "stopped on a limit, no verdict reached".
_loki_print_interrupted_resume_hint() {
    local status
    status="$(_loki_resolve_run_status 2>/dev/null)" || return 1
    case "$status" in
        interrupted|max_iterations_reached|budget_exceeded) ;;
        *) return 1 ;;
    esac

    local state_file="$LOKI_DIR/autonomy-state.json"
    if [ -n "${LOKI_SESSION_ID:-}" ] && [ -f "$LOKI_DIR/sessions/${LOKI_SESSION_ID}/autonomy-state.json" ]; then
        state_file="$LOKI_DIR/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
    fi
    [ -f "$state_file" ] || return 1

    # iteration, lastRun and prdPath in one read. Tab-separated so a spec path
    # containing spaces survives; python3 is already a hard dep of this file.
    local fields iteration last_run prd_path
    fields="$(_LOKI_RH_STATE="$state_file" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['_LOKI_RH_STATE']))
except Exception:
    d = {}
# save_state() writes "iteration" (autonomy/run.sh:6707). Reading only
# "iterationCount" made every REAL interrupted run report iteration 0 -- the
# one number that makes this message reassuring rather than alarming. The
# fixture that first exercised this used the other spelling, so the test
# passed while the shipped path was wrong. Accept both, real key first.
print('\t'.join([
    str(d.get('iteration', d.get('iterationCount', 0))),
    str(d.get('lastRun', '') or 'unknown'),
    str(d.get('prdPath', '') or ''),
]))" 2>/dev/null)" || return 1
    IFS=$'\t' read -r iteration last_run prd_path <<< "$fields"

    # The spec path is what makes the command RESUME this run rather than start a
    # different one, so only interpolate it when it still exists on disk. A stale
    # path would produce a command that fails on paste.
    local resume_cmd="loki start"
    if [ -n "$prd_path" ] && [ -f "$prd_path" ]; then
        resume_cmd="loki start \"$prd_path\""
    fi

    # A capped run needs DIFFERENT advice than an interrupted one: re-running it
    # unchanged hits the very same limit on the next iteration. cmd_next's
    # action_text already says to raise the cap first; saying less here would
    # move the contradiction rather than close it.
    case "$status" in
        max_iterations_reached)
            # Deliberately does NOT print the iteration it reached.
            # `load_state` resets ITERATION_COUNT=0 for this terminal status
            # (autonomy/run.sh, the failure-terminals case arm), so a fresh
            # `loki start` is a NEW session from iteration 0. Advertising the
            # old number would promise a continuation the runtime will not
            # honour -- the exact false claim tests/test-resume-discoverability.sh
            # guards, and it caught this in CI.
            echo -e "${YELLOW}Iteration limit reached.${NC} (last activity: ${last_run})."
            echo "The committed work is intact, but this starts a NEW session from"
            echo "iteration 0 -- it does not continue the old count."
            echo "Raise the cap (or narrow the spec), then run:"
            echo ""
            echo -e "  ${BOLD}LOKI_MAX_ITERATIONS=<higher> ${resume_cmd}${NC}"
            ;;
        budget_exceeded)
            # Same reasoning as max_iterations_reached: this terminal status is
            # in load_state's reset list, so the prior iteration count is gone.
            echo -e "${YELLOW}Budget limit reached.${NC} (last activity: ${last_run})."
            echo "The committed work is intact, but this starts a NEW session from"
            echo "iteration 0 -- it does not continue the old count."
            echo "Raise the budget (or narrow the spec), then run:"
            echo ""
            echo -e "  ${BOLD}LOKI_BUDGET_LIMIT=<higher> ${resume_cmd}${NC}"
            ;;
        *)
            echo -e "${YELLOW}Interrupted run found.${NC} Stopped at iteration ${BOLD}${iteration}${NC} (last activity: ${last_run})."
            echo "Saved progress is intact. Resume it with:"
            echo ""
            echo -e "  ${BOLD}${resume_cmd}${NC}"
            ;;
    esac
    echo ""
    # Only an INTERRUPTED run actually resumes its iteration count. The capped
    # terminals above are reset to 0 by load_state, so the pick-up sentence must
    # not be printed for them.
    if [ "$status" = "interrupted" ]; then
        echo -e "${DIM}It picks up from iteration ${iteration}; verification re-runs, so nothing inherits a stale PASS.${NC}"
    else
        echo -e "${DIM}Verification re-runs from scratch, so nothing inherits a stale PASS.${NC}"
    fi
    return 0
}

# Resume paused execution
cmd_resume() {
    # v7.6.2 B-13 fix: --help must print help, not act on session state.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- resume a paused or interrupted session${NC}"
            echo ""
            echo "Usage: loki resume [options]"
            echo ""
            echo "Clears the paused flag and lets the autonomous runner continue"
            echo "from where 'loki pause' stopped it."
            echo ""
            echo "If the run was interrupted instead (Ctrl-C, crash, closed laptop)"
            echo "there is no flag to clear, so this prints the iteration it reached"
            echo "and the exact command to continue from that saved state."
            echo ""
            echo "Options:"
            echo "  --help, -h   Show this help and exit"
            echo ""
            echo "See also: loki pause, loki start"
            return 0
            ;;
    esac
    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${YELLOW}No .loki directory found.${NC}"
        echo "No session to resume. Start a session with: loki start"
        exit 0
    fi

    # P12-10b: a Loki 10 run paused by `loki pause` is released by clearing its request.
    if _loki_e10_active_run pause; then
        local _rz_rc=0
        ( LOKI_DIR="$LOKI_DIR" _loki_bun_delegate resume ) || _rz_rc=$?
        [ "$_rz_rc" -ne 0 ] || return 0
    fi

    # RECOVERY: an interrupted engine10 run is resumed at its phase by loki-ts/src/util/run_outputs.ts
    # (exit 0 resumed, 4 phase reported, 5 publish failed, anything else: nothing for it, fall through).
    if ls "$LOKI_DIR"/runs/e10-*/events.jsonl >/dev/null 2>&1; then
        local _e10_rc=0
        ( _loki_bun_delegate engine10 resume "$@" ) || _e10_rc=$?
        if _loki_e10_resume_map "$_e10_rc"; then return "$_LOKI_E10_RC"; fi
    fi

    # Check if there is anything to resume before touching signal files
    local has_pause_signal=false
    local has_stop_signal=false
    [ -f "$LOKI_DIR/PAUSE" ] && has_pause_signal=true
    [ -f "$LOKI_DIR/STOP" ] && has_stop_signal=true
    local session_running=false
    is_session_running && session_running=true

    # If nothing is paused/stopped and no session is running, the run may still
    # be RESUMABLE: a Ctrl-C / crash / closed laptop leaves no PAUSE or STOP
    # file, only autonomy-state.json with status "interrupted" and the iteration
    # it reached. This branch used to print "No session to resume" and send the
    # user to `loki start`, hiding real saved progress -- and because
    # `loki next` maps interrupted -> cmd_resume, the one command whose job is
    # "do the right next thing" hit the same dead end. Surface it instead.
    if ! $has_pause_signal && ! $has_stop_signal && ! $session_running; then
        if _loki_print_interrupted_resume_hint; then
            return 0
        fi
        echo -e "${YELLOW}No session to resume.${NC}"
        echo "Start a session with: loki start"
        exit 0
    fi

    # Clear the signal file if one exists
    local removed_signal=""
    if $has_pause_signal; then
        rm -f "$LOKI_DIR/PAUSE"
        removed_signal="PAUSE"
    elif $has_stop_signal; then
        rm -f "$LOKI_DIR/STOP"
        removed_signal="STOP"
    fi

    if [ -n "$removed_signal" ]; then
        # Emit session resume event
        emit_event session cli resume "cleared_signal=$removed_signal"
        # Emit success pattern for resume (SYN-018)
        emit_learning_signal success_pattern \
            --source cli \
            --action "session_resume" \
            --pattern-name "resume_after_$removed_signal" \
            --action-sequence '["check_signals", "clear_signal", "resume"]' \
            --outcome success \
            --confidence 0.85
        if $session_running; then
            echo -e "${GREEN}$removed_signal signal cleared. Session will resume automatically.${NC}"
        else
            echo -e "${GREEN}$removed_signal signal cleared.${NC}"
            echo "Session is not running. Start with: loki start"
        fi
    else
        # Session is running but no signals to clear
        echo -e "${CYAN}Session is running normally. Nothing to resume.${NC}"
    fi
}

# Resolve the terminal-run status from the same already-captured artifacts that
# cmd_why reads (no new state, no spend). Echoes a single token: the resolved
# status string (e.g. council_approved, paused, max_iterations_reached), or the
# literal "none" when no run exists here yet. Used by cmd_next so its status->
# action mapping reads exactly the same state cmd_why does (no drift).
# Read-only; never fabricates.
_loki_resolve_run_status() {
    local loki_dir="${LOKI_DIR:-.loki}"
    local state_file="$loki_dir/autonomy-state.json"
    if [ -n "${LOKI_SESSION_ID:-}" ] && [ -f "$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json" ]; then
        state_file="$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
    fi
    local completion_file="$loki_dir/state/completion.json"

    if [ ! -f "$state_file" ] && [ ! -f "$completion_file" ]; then
        printf '%s\n' "none"
        return 0
    fi

    _LOKI_NA_STATE="$state_file" _LOKI_NA_COMPLETION="$completion_file" python3 - <<'NASTATUS'
import json, os
def load(p):
    try:
        with open(p) as f: return json.load(f)
    except Exception: return {}
state = load(os.environ.get("_LOKI_NA_STATE", ""))
comp = load(os.environ.get("_LOKI_NA_COMPLETION", ""))
# Mirror cmd_why's COMP_OUTCOME_ALIASES so a completion-only state (a rotated or
# crashed run with no live status, only completion.json's outcome) resolves to
# the canonical status loki next's action map keys on. Without this, an outcome
# of "complete"/"max_iterations" drifted to the default "loki status" instead of
# "loki ship"/"loki resume".
COMP_OUTCOME_ALIASES = {
    "complete": "council_approved",
    "max_iterations": "max_iterations_reached",
    "intervention": "paused",
}
status = state.get("status")
if not status:
    raw_outcome = comp.get("outcome") or ""
    status = COMP_OUTCOME_ALIASES.get(raw_outcome, raw_outcome) or "unknown"
print(status)
NASTATUS
}

# Map a resolved status to the single concrete next-action SENTENCE (the same
# wording cmd_why prints under "What to do"). Echoes the action string. Kept in
# lockstep with cmd_why's GUIDE map so why and next agree.
_loki_next_action() {
    local status="${1:-unknown}"
    case "$status" in
        council_approved)
            echo "Review the diff and open a PR (loki ship, or git push + gh pr create)." ;;
        council_force_approved)
            echo "Review the diff carefully before merging -- convergence was not unanimous." ;;
        completion_promise_fulfilled)
            echo "Verify the promised outcome, then review and PR (loki ship)." ;;
        max_iterations_reached)
            echo "Raise LOKI_MAX_ITERATIONS (or narrow the spec), then resume (loki resume)." ;;
        max_retries_exceeded)
            echo "Read .loki/logs for the recurring error, fix the root cause, then re-run (loki why)." ;;
        failed)
            echo "Read the failure (loki why), fix it, then re-run." ;;
        policy_blocked)
            echo "Review the blocking finding (loki why); address it or use the documented override." ;;
        budget_exceeded)
            echo "Raise LOKI_BUDGET_LIMIT or accept the partial result, then resume (loki resume)." ;;
        paused)
            echo "Resume with: loki resume." ;;
        interrupted)
            echo "Resume with: loki resume." ;;
        stopped)
            echo "Start a new build (loki start), or resume if you meant to continue." ;;
        force_stopped)
            echo "Start a new build, or investigate why a force-stop was needed (loki why)." ;;
        running)
            echo "A build looks active (or crashed mid-run); check loki status." ;;
        none)
            echo "Run a build first: loki start <spec>." ;;
        *)
            echo "Check loki status and loki why for detail." ;;
    esac
}

# loki next --suggest: with no active run, propose up to 3 small tasks drawn
# from open GitHub issues (only when gh is authenticated), TODO/FIXME lines and
# the last recorded failing test run. est_usd comes from the one estimator
# ('loki estimate --json', cost_preview.ts) and is null / NOT AVAILABLE when it
# has no usable history (Engine Law L0: never a number from task text).
# Sources are interleaved so one noisy source cannot fill all 3.
_loki_next_suggest() {
    local as_json=false
    case "${1:-}" in
        --json) as_json=true ;;
        "") : ;;
        *) echo "Usage: loki next --suggest [--json]" >&2; return 1 ;;
    esac
    local status
    status="$(_loki_resolve_run_status)"
    if [ "$status" = "running" ]; then
        echo "loki next --suggest: a run is active here; use 'loki next' instead." >&2
        return 1
    fi
    local tmp note=""
    tmp="$(mktemp -d "${TMPDIR:-/tmp}/loki-run.XXXXXXXX")" || return 1
    : > "$tmp/issues"; : > "$tmp/todos"; : > "$tmp/tests"
    # gh runs under a python timeout so a hung network call cannot hang the command.
    python3 - > "$tmp/issues" 2>/dev/null <<'PYEOF' || note="issues skipped: gh not installed, not authenticated, or timed out"
import json, os, subprocess, sys
def _t(name, default):
    try:
        return float(os.environ.get(name) or default)
    except ValueError:
        return default
try:
    if subprocess.run(["gh", "auth", "status"], capture_output=True, timeout=_t("LOKI_NEXT_GH_AUTH_TIMEOUT", 10)).returncode != 0:
        sys.exit(1)
    r = subprocess.run(["gh", "issue", "list", "--state", "open", "--limit", "10", "--json", "number,title"],
                       capture_output=True, text=True, timeout=_t("LOKI_NEXT_GH_LIST_TIMEOUT", 15))
    if r.returncode != 0:
        sys.exit(1)
    for i in json.loads(r.stdout):
        print("#%s %s" % (i["number"], str(i["title"]).replace("\t", " ")))
except Exception:
    sys.exit(1)
PYEOF
    local est_json="" est_pid="" est_ticks=0 est_max est_kid
    est_max=$(( ${LOKI_NEXT_ESTIMATE_TIMEOUT:-20} * 4 ))
    # The estimate runs in the background and is polled, so a slow or hung
    # estimator cannot hang the command (no timeout binary is assumed: macOS
    # bash 3.2 may lack one). Only the PID started here, and its direct
    # children, are ever signalled.
    ( cmd_estimate --json > "$tmp/est" 2>/dev/null ) &
    est_pid=$!
    while kill -0 "$est_pid" 2>/dev/null; do
        if [ "$est_ticks" -ge "$est_max" ]; then
            for est_kid in $(pgrep -P "$est_pid" 2>/dev/null); do kill "$est_kid" 2>/dev/null || true; done
            kill "$est_pid" 2>/dev/null || true
            note="${note:+$note; }estimate unavailable: timed out after ${LOKI_NEXT_ESTIMATE_TIMEOUT:-20}s"
            : > "$tmp/est"
            break
        fi
        sleep 0.25
        est_ticks=$((est_ticks + 1))
    done
    wait "$est_pid" 2>/dev/null || true
    est_json="$(cat "$tmp/est" 2>/dev/null)" || est_json=""
    { git grep -nIE 'TODO|FIXME' -- . ':!.loki' 2>/dev/null \
        || grep -rnIE 'TODO|FIXME' --exclude-dir=.git --exclude-dir=.loki --exclude-dir=node_modules . 2>/dev/null; } \
        | head -20 > "$tmp/todos" || true
    python3 - "${LOKI_DIR:-.loki}/quality/test-results.json" > "$tmp/tests" 2>/dev/null <<'PYEOF' || true
import json, sys
try:
    d = json.load(open(sys.argv[1]))
    if d.get("pass") in (False, "false", "False"):
        print("failing tests (%s): %s" % (d.get("runner", "unknown"), str(d.get("summary", ""))[:120].replace("\n", " ")))
except Exception:
    pass
PYEOF
    SUGGEST_JSON=$as_json SUGGEST_NOTE="$note" SUGGEST_EST="$est_json" python3 - "$tmp" <<'PYEOF'
import json, os, sys
d = sys.argv[1]
def rd(n):
    return [l.rstrip("\n") for l in open(os.path.join(d, n)) if l.strip()]
est, na = None, "estimator returned nothing"
try:
    r = json.loads(os.environ.get("SUGGEST_EST", ""))
    if r.get("ok") is True and isinstance(r.get("est", {}).get("usd"), list):
        est = r["est"]["usd"]
    else:
        na = str(r.get("reason", "no usable run history"))
except Exception:
    pass
src = [("failing_test", "small", rd("tests")), ("issue", "medium", rd("issues")), ("todo", "small", rd("todos"))]
out = []
while len(out) < 3 and any(x[2] for x in src):
    for name, size, items in src:
        if items and len(out) < 3:
            out.append({"source": name, "task": items.pop(0), "size": size, "est_usd": est})
note = os.environ.get("SUGGEST_NOTE", "")
if os.environ.get("SUGGEST_JSON") == "true":
    print(json.dumps({"suggestions": out, "notes": [note] if note else [], "est_usd_reason": None if est else na}))
else:
    if note:
        print("note: " + note)
    if not out:
        print("No suggestions found (no open issues, TODO/FIXME lines or failing tests).")
    for i, o in enumerate(out, 1):
        e = ("est_usd %.2f-%.2f" % tuple(o["est_usd"])) if o["est_usd"] else "est_usd NOT AVAILABLE (%s)" % na
        print("%d. [%s] %s (%s)" % (i, o["source"], o["task"], e))
PYEOF
    rm -rf "$tmp"
    return 0
}

# loki next -- one command that runs the right next step for you.
# Reads the SAME already-captured state as cmd_why (no new state, no spend),
# resolves the terminal status, and proposes the single mapped next step, then
# runs it on confirmation. Default on a TTY is confirm-then-run; --yes runs it
# without asking; --dry-run and any non-TTY context only PRINT (honest: never
# auto-acts in CI / when piped). Strictly additive: reuses cmd_resume, cmd_ship,
# cmd_why, and cmd_status; never touches the build path.
cmd_next() {
    local assume_yes=false
    local dry_run=false
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- run the right next step for you${NC}"
            echo ""
            echo "Usage: loki next [--yes] [--dry-run] [--suggest [--json]]"
            echo ""
            echo "Reads the last build's recorded outcome (the same state 'loki why'"
            echo "reads -- no new run, no cost) and carries you forward by running the"
            echo "single mapped next step: resume a paused/interrupted run, finish a"
            echo "council-approved build (loki ship), or route a failure to 'loki why'."
            echo ""
            echo "Default on a terminal: it shows the proposed step and asks before"
            echo "running it. Non-interactive (piped/CI) it only prints the step;"
            echo "it never acts on its own there."
            echo ""
            echo "Options:"
            echo "  --yes        Run the proposed step without asking"
            echo "  --dry-run    Print the proposed step only; do not run it"
            echo "  --suggest    No active run: propose up to 3 small tasks (issues, TODO/FIXME, failing tests)"
            echo "  --help, -h   Show this help and exit"
            return 0
            ;;
        --suggest) shift; _loki_next_suggest "$@"; return $? ;;
        --yes|-y) assume_yes=true ;;
        --dry-run) dry_run=true ;;
        "") : ;;
        *) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki next [--yes] [--dry-run] [--suggest [--json]]" >&2; return 1 ;;
    esac
    # Allow a second flag (e.g. --dry-run --yes order-agnostic).
    case "${2:-}" in
        --yes|-y) assume_yes=true ;;
        --dry-run) dry_run=true ;;
    esac

    local status
    status="$(_loki_resolve_run_status)"
    if [ "$status" = "none" ]; then
        echo "loki next: no run found here yet." >&2
        echo "Run a build first: loki start <spec>" >&2
        return 1
    fi

    local action_text
    action_text="$(_loki_next_action "$status")"

    # Map the status to the concrete callable that carries the user forward.
    # action_cmd is a human label; the case below dispatches to the real function.
    local action_cmd=""
    case "$status" in
        paused|interrupted)                              action_cmd="loki resume" ;;
        council_approved|council_force_approved|completion_promise_fulfilled)
                                                         action_cmd="loki ship" ;;
        max_iterations_reached|budget_exceeded)          action_cmd="loki resume" ;;
        failed|max_retries_exceeded|policy_blocked|force_stopped)
                                                         action_cmd="loki why" ;;
        *)                                               action_cmd="loki status" ;;
    esac

    echo -e "${BOLD}Loki: next${NC}"
    echo "  Last outcome : $status"
    echo "  Next step    : $action_text"
    echo "  Will run     : $action_cmd"
    echo ""

    # Honesty gate: only ACT on an interactive terminal with consent, or with
    # --yes. --dry-run and non-TTY always print-only.
    if [ "$dry_run" = true ]; then
        echo "(dry-run: not running. Run '$action_cmd' yourself, or 'loki next --yes'.)"
        return 0
    fi
    if [ "$assume_yes" != true ]; then
        if [ ! -t 0 ] || [ ! -t 1 ]; then
            echo "(non-interactive: not running. Run '$action_cmd', or 'loki next --yes'.)"
            return 0
        fi
        local reply=""
        printf "Run '%s' now? [y/N] " "$action_cmd"
        read -r reply || reply=""
        case "$reply" in
            y|Y|yes|YES) : ;;
            *) echo "Skipped. Run '$action_cmd' when ready."; return 0 ;;
        esac
    fi

    # Dispatch to the real command. For budget/iteration caps, print the env-var
    # guidance first (the resume alone will not lift the cap), then resume.
    case "$status" in
        max_iterations_reached)
            echo "Note: raise the cap first if you want more iterations, e.g.:"
            echo "  LOKI_MAX_ITERATIONS=<n> loki resume"
            echo ""
            ;;
        budget_exceeded)
            echo "Note: raise the budget first if you want to continue, e.g.:"
            echo "  LOKI_BUDGET_LIMIT=<usd> loki resume"
            echo ""
            ;;
    esac
    case "$action_cmd" in
        "loki resume")  cmd_resume ;;
        "loki ship")    cmd_ship ;;
        "loki why")     cmd_why ;;
        *)              cmd_status ;;
    esac
    return $?
}

# loki ship -- the build is done, now get it out (review + PR advice in one flow).
# Publish an already-prepared PR. This is deliberately a separate, explicit
# mutation boundary from the default print-only `loki ship` flow. Preparation
# writes the exact reviewed title/body under .loki/state; publishing consumes
# those bytes without rebuilding, re-rendering, or inventing evidence.
publish_prepared_pr() {
    local state_dir="${LOKI_DIR:-.loki}/state"
    local title_file="$state_dir/pr-title.txt"
    local body_file="$state_dir/pr-body.md"
    local pr_state="$state_dir/pr.json"

    if [ ! -s "$title_file" ] || [ ! -s "$body_file" ]; then
        echo "Error: no prepared PR found. Run 'loki start <issue> --prepare-pr' first." >&2
        return 2
    fi
    if ! command -v git >/dev/null 2>&1 || ! git rev-parse --git-dir >/dev/null 2>&1; then
        echo "Error: publishing a prepared PR requires a git repository." >&2
        return 2
    fi
    if ! command -v gh >/dev/null 2>&1; then
        echo "Error: publishing a prepared PR requires the GitHub CLI (gh)." >&2
        return 2
    fi

    local branch base title existing_url pr_url tmp_state
    branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
    case "$branch" in
        ""|HEAD|main|master)
            echo "Error: refusing to publish prepared work from '$branch'; check out its issue branch." >&2
            return 2 ;;
    esac
    if ! git remote get-url origin >/dev/null 2>&1; then
        echo "Error: publishing a prepared PR requires an 'origin' remote." >&2
        return 2
    fi
    title="$(cat "$title_file")"
    if [ -z "$title" ]; then
        echo "Error: prepared PR title must be one non-empty line." >&2
        return 2
    fi
    case "$title" in
        *$'\n'*)
            echo "Error: prepared PR title must be one non-empty line." >&2
            return 2 ;;
    esac

    # Idempotence: an interrupted/retried publish reuses the open PR and never
    # creates a duplicate. This read-only query occurs only after --publish.
    existing_url="$(gh pr list --head "$branch" --state open --json url --jq '.[0].url // empty' 2>/dev/null || true)"
    if [ -n "$existing_url" ]; then
        pr_url="$existing_url"
    else
        # Do not attempt PR creation after a failed push. The prepared body
        # remains local and byte-identical, so the operator can fix auth/network
        # and retry. A successful push followed by create failure is reversible
        # with the exact command printed below.
        if ! git push --set-upstream origin "$branch"; then
            echo "Error: branch push failed; prepared PR files were preserved." >&2
            return 1
        fi
        if ! pr_url="$(gh pr create --head "$branch" --title "$title" --body-file "$body_file")"; then
            echo "Error: PR creation failed; prepared PR files were preserved." >&2
            echo "Rollback remote branch: git push origin --delete $branch" >&2
            return 1
        fi
        pr_url="$(printf '%s\n' "$pr_url" | grep -Eo 'https://[^[:space:]]+' | head -1)"
        if [ -z "$pr_url" ]; then
            echo "Error: gh reported success without a PR URL; prepared files were preserved." >&2
            echo "Inspect: gh pr list --head $branch" >&2
            return 1
        fi
    fi

    mkdir -p "$state_dir" || return 1
    tmp_state="$pr_state.tmp.$$"
    if command -v jq >/dev/null 2>&1; then
        jq -n --arg url "$pr_url" --arg branch "$branch" --arg title "$title" \
            '{state:"created", url:$url, branch:$branch, title:$title,
              body_path:".loki/state/pr-body.md"}' > "$tmp_state" \
            && mv "$tmp_state" "$pr_state" || { rm -f "$tmp_state"; return 1; }
    fi
    echo "PR ready: $pr_url"
}

# Composes existing surfaces only: optionally opens the running app (--preview),
# runs the SAME diff quality gates as cmd_review, and on a clean-enough result
# prints the exact branch-aware PR command via the shared print_pr_advice (the
# same advisory cmd_deploy uses). PRINT-ONLY for shipping: it NEVER pushes and
# NEVER deploys; it prints (and clipboards, as the advisory already does) the
# git push + PR command for YOU to run. If review finds HIGH/CRITICAL it stops
# and points at the findings instead of advising a PR.
cmd_ship() {
    local do_preview=false
    local publish=false
    local review_args=()
    # Tracks whether the caller passed an explicit scope flag (--staged, --since,
    # ...). Passthrough flags like --yes/--severity do NOT set this, so they do
    # not suppress the clean-loki-branch auto-scoping below.
    local ship_explicit_scope=false
    while [ $# -gt 0 ]; do
        case "${1:-}" in
            --help|-h|help)
                echo -e "${BOLD}Loki Mode -- finish the build: review, then PR advice (print-only)${NC}"
                echo ""
                echo "Usage: loki ship [--preview] [--yes] [review options]"
                echo "       loki ship --publish"
                echo ""
                echo "Runs the natural finish line in one command:"
                echo "  1. (with --preview) opens the app Loki built and started locally"
                echo "  2. runs 'loki review' on your uncommitted/branch diff (the same"
                echo "     quality gates)"
                echo "  3. on a clean-enough result, PRINTS the exact branch-aware"
                echo "     git push + pull-request command for YOU to run"
                echo ""
                echo "The default flow is advisory only: it NEVER runs 'git push' or"
                echo "creates a PR. --publish is the explicit mutation boundary: it"
                echo "publishes the exact title/body previously written by --prepare-pr."
                echo "If review finds HIGH or CRITICAL findings it stops and points you"
                echo "at the findings instead of advising a PR (exit code 1)."
                echo ""
                echo "Options:"
                echo "  --preview    Open the running local app first (loki preview)"
                echo "  --publish    Push the current issue branch and publish its prepared PR"
                echo "  --yes, -y    Pass through to review (skip its confirmation prompt)"
                echo "  --help, -h   Show this help and exit"
                echo ""
                echo "Any other options are passed through to 'loki review'"
                echo "(e.g. --staged, --since <commit>, --severity high)."
                return 0
                ;;
            --preview) do_preview=true ;;
            --publish) publish=true ;;
            # Only a known set of NON-scope passthrough flags are exempt from
            # "explicit scope". Everything else the caller passes -- an explicit
            # scope flag (--staged/--since/...), a positional <file-or-dir>, or any
            # other review flag that narrows WHAT is reviewed -- counts as explicit
            # scope, so we do NOT override it with the clean-branch auto-scoping
            # below. This is the safe default: passthrough-that-changes-scope must
            # win over our auto-scope. The exempt list is only flags that never
            # change scope: --yes/-y (skip prompt), --severity (threshold).
            # (Without this, `loki ship somefile.py` on a clean loki branch would
            # get --since base injected and review the whole range, not the file.)
            --yes|-y|--severity=*)
                review_args+=("$1") ;;
            # --severity in its DOCUMENTED two-token form (`--severity high`) takes
            # a VALUE in the next arg. Consume both here so the value does not fall
            # to the catch-all and wrongly mark explicit scope (which would skip the
            # clean-branch auto-scoping and re-introduce the empty-diff false-clean
            # bug). --severity sets a threshold, not a scope, so neither token is
            # explicit scope.
            --severity)
                review_args+=("$1")
                if [ "$#" -ge 2 ] && [ "${2#-}" = "$2" ]; then
                    review_args+=("$2"); shift
                fi
                ;;
            *) review_args+=("$1"); ship_explicit_scope=true ;;
        esac
        shift
    done

    if [ "$publish" = true ]; then
        if [ "$do_preview" = true ] || [ "${#review_args[@]}" -gt 0 ]; then
            echo "Error: --publish cannot be combined with preview/review options." >&2
            return 2
        fi
        publish_prepared_pr
        return $?
    fi

    # 1. Optional preview of the running local app. Only when an app-runner state
    #    file exists, so we never imply an app is running when it is not.
    if [ "$do_preview" = true ]; then
        local loki_dir="${LOKI_DIR:-.loki}"
        if [ -f "$loki_dir/state/app-runner.json" ] || [ -f "$loki_dir/app-runner.json" ]; then
            echo -e "${BOLD}Opening the running app...${NC}"
            cmd_preview || true
            echo ""
        else
            echo "(no running app detected; skipping preview. Start one with loki start.)"
            echo ""
        fi
    fi

    # 2. Run the SAME diff quality gates as cmd_review. Capture its documented
    #    exit code (0 clean, 1 HIGH, 2 CRITICAL). set -e safe: capture via || rc=$?.
    #
    #    Scope correction: cmd_review defaults to the UNCOMMITTED diff, but run.sh
    #    auto-commits the session's work to the loki/session-* branch, so by the
    #    time `loki ship` runs the tree is usually CLEAN -- which would make review
    #    see an empty diff and falsely report "clean" before advising a PR (the
    #    safety gate would be a no-op exactly when it matters). When the caller
    #    gave no explicit scope AND we are on a loki/* branch with a clean tree,
    #    default the scope to the branch-vs-base RANGE (the work being shipped),
    #    resolved the same way the PR advice below resolves it.
    local _ship_dir="."
    if [ "$ship_explicit_scope" = false ]; then
        local _ship_head="" _ship_base="" _ship_dirty=""
        _ship_head="$(git -C "$_ship_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
        # Untracked files (e.g. a gitignored .loki/) do not count as "dirty" --
        # what matters is whether the shippable TRACKED work is already committed.
        _ship_dirty="$(git -C "$_ship_dir" status --porcelain --untracked-files=no 2>/dev/null)"
        case "$_ship_head" in
            loki/*)
                if [ -z "$_ship_dirty" ]; then
                    if [ -s "$_ship_dir/.loki/state/base-branch.txt" ]; then
                        _ship_base="$(head -n1 "$_ship_dir/.loki/state/base-branch.txt" 2>/dev/null || echo "")"
                    fi
                    if [ -z "$_ship_base" ]; then
                        local _ship_oh=""
                        _ship_oh="$(git -C "$_ship_dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")"
                        [ -n "$_ship_oh" ] && _ship_base="${_ship_oh##*/}"
                    fi
                    if [ -n "$_ship_base" ] && git -C "$_ship_dir" rev-parse --verify --quiet "$_ship_base" >/dev/null 2>&1; then
                        review_args+=("--since" "$_ship_base")
                        echo -e "${DIM}Clean working tree on $_ship_head; reviewing the branch range vs $_ship_base.${NC}"
                    fi
                fi
                ;;
        esac
    fi
    echo -e "${BOLD}Running quality gates on the diff (loki review)...${NC}"
    echo ""
    local rc=0
    # Bash 3.2 treats an empty-array expansion as an unbound variable under
    # `set -u`. macOS still ships that shell, so the documented zero-option
    # `loki ship` path must call review without expanding an empty array.
    if [ "${#review_args[@]}" -gt 0 ]; then
        cmd_review "${review_args[@]}" || rc=$?
    else
        cmd_review || rc=$?
    fi

    if [ "$rc" -ge 1 ]; then
        echo ""
        if [ "$rc" -ge 2 ]; then
            echo -e "${RED}Review found CRITICAL findings. Not advising a PR yet.${NC}" >&2
        else
            echo -e "${YELLOW}Review found HIGH findings. Not advising a PR yet.${NC}" >&2
        fi
        echo "Fix the findings above (or run 'loki review' for the full report), then 'loki ship' again." >&2
        return 1
    fi

    # 3. Clean enough: print the branch-aware PR command (the SAME advisory
    #    cmd_deploy uses). Derive base/head exactly as cmd_deploy does.
    echo ""
    echo -e "${GREEN}Review is clean (no HIGH or CRITICAL findings).${NC}"
    echo ""
    local dir="."
    local head="" base=""
    head="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
    [ -n "$head" ] || head="HEAD"
    case "$head" in
        loki/*)
            if [ -s "$dir/.loki/state/base-branch.txt" ]; then
                base="$(head -n1 "$dir/.loki/state/base-branch.txt" 2>/dev/null || echo "")"
            fi
            ;;
    esac
    if [ -z "$base" ]; then
        local origin_head=""
        origin_head="$(git -C "$dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")"
        if [ -n "$origin_head" ]; then
            base="${origin_head##*/}"
        fi
    fi

    if declare -F print_pr_advice >/dev/null 2>&1; then
        if [ -n "$base" ]; then
            print_pr_advice "$base" "$head" "$dir"
        else
            echo "Could not determine the PR base branch automatically; set it manually:"
            print_pr_advice "manually-set-base" "$head" "$dir"
        fi
    else
        echo "To open a pull request:"
        echo "  git push -u origin ${head}"
        echo "  Open a pull request for ${head} (set the base branch manually)."
    fi
    echo ""
    echo -e "${DIM}Loki does not push or deploy for you; run the command above when ready.${NC}"
    return 0
}

# Per-error_class remediation, shared by BOTH cmd_why paths (human + --json).
#
# WHY THIS IS A SHELL VARIABLE. cmd_why runs two SEPARATE python3 heredocs, so a
# helper defined in one is invisible to the other. Copy-pasting the map into
# both is exactly the drift this file warns against everywhere else (see the
# GUIDE / _loki_next_action "kept in lockstep" comments). Defining it once and
# injecting it into both makes divergence impossible rather than merely
# discouraged.
#
# SCOPE. The GUIDE map keys on run STATUS (council_approved / failed / ...).
# This keys on the LAST_ERROR error_class -- a different, orthogonal axis. A
# `failed` run got the same "read the logs" line whether the cause was a 401 or
# a timeout; this is what makes the two cases differ.
#
# run.sh WRITES the classification (_loki_classify_iteration_error, :1857) and
# injects a heal hint naming the class (:1889), but neither carries remediation
# text -- so there is no second surface to mirror. This is the sole home for the
# wording. If run.sh ever gains one, keep them in lockstep.
#
# HONESTY. last_error_action() returns None for any class not in the map --
# including "unknown", which run.sh writes when it will not guess. A None means
# the caller must print the recorded brief verbatim and infer NOTHING. Never add
# a fallback action here: a generic string on an unknown class is a fabricated
# diagnosis wearing an action's clothes.
read -r -d '' _LOKI_WHY_ACTIONS_PY <<'WHYACTIONS' || true
# error_class -> (action). Each is a command or env var the reader can act on.
LAST_ERROR_ACTIONS = {
    "rate_limited":
        "Wait for the provider limit to reset, then re-run: loki start <spec>",
    "build_timeout":
        "Raise the per-iteration limit, or narrow the spec so one iteration does less:\n"
        "LOKI_ITERATION_TIMEOUT=3600 loki start <spec>",
    "provider_empty_output":
        "The provider returned nothing. Check provider health first: loki doctor\n"
        "then re-run. If doctor is clean, try another provider: LOKI_PROVIDER=<name> loki start <spec>",
}

# Auth remediation is PROVIDER-SPECIFIC and the LAST_ERROR schema records no
# provider. So we use LOKI_PROVIDER when it is set, and when it is not we list
# every option LABELLED rather than presenting one provider's fix as the answer.
PROVIDER_AUTH = {
    "claude": "claude login   (or set ANTHROPIC_API_KEY)",
    "codex": "codex login   (or set OPENAI_API_KEY)",
    "aider": "set the API key for your aider model (OPENAI_API_KEY / ANTHROPIC_API_KEY)",
    "cline": "re-enter the API key in Cline's provider settings",
    "opencode": "opencode auth login",
}


def last_error_action(rec, provider=""):
    """Concrete next action for a LAST_ERROR record, or None if unrecognized.

    None is a real answer: it means we have no mapped action and the caller must
    fall back to the recorded brief verbatim. Do not turn it into a default.
    """
    if not isinstance(rec, dict):
        return None
    ec = str(rec.get("error_class") or "").strip()
    if ec == "auth_error":
        p = str(provider or "").strip().lower()
        if p in PROVIDER_AUTH:
            return "Re-authenticate %s: %s" % (p, PROVIDER_AUTH[p])
        lines = ["Re-authenticate the provider in use (LOKI_PROVIDER is unset, so "
                 "the provider was not recorded):"]
        for _n in sorted(PROVIDER_AUTH):
            lines.append("  %-9s %s" % (_n, PROVIDER_AUTH[_n]))
        return "\n".join(lines)
    action = LAST_ERROR_ACTIONS.get(ec)
    if action is None:
        return None
    if ec == "rate_limited":
        # retry_after is NOT in the documented schema. Read it defensively and
        # print nothing when absent -- a placeholder would read as measured.
        _r = rec.get("retry_after", rec.get("retry_after_seconds"))
        if _r not in (None, ""):
            action += "\nProvider reported retry-after: %s" % _r
    return action
WHYACTIONS

# loki why -- actionable failure/outcome diagnosis (B5).
# Reads the already-captured run artifacts (no new state): the terminal run state
# (.loki/<autonomy-state>.json: status, lastExitCode, iterationCount), the durable
# completion record (.loki/state/completion.json: outcome, branch, files, pr_url),
# and the latest structured handoff (.loki/memory/handoffs/*.md). Produces an
# honest "what happened + what to do" report. Read-only; never fabricates -- if a
# field was not captured it says so. --json emits the machine-readable record.
cmd_why() {
    local as_json=0
    case "${1:-}" in
        --json) as_json=1 ;;
        --help|-h) echo "Usage: loki why [--json]  -- explain the last build's outcome and what to do next"; return 0 ;;
        "" ) : ;;
        *) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki why [--json]" >&2; return 1 ;;
    esac

    local loki_dir="${LOKI_DIR:-.loki}"
    # Session-namespaced state when LOKI_SESSION_ID is set (mirrors run.sh A6).
    local state_file="$loki_dir/autonomy-state.json"
    if [ -n "${LOKI_SESSION_ID:-}" ] && [ -f "$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json" ]; then
        state_file="$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
    fi
    local completion_file="$loki_dir/state/completion.json"

    if [ ! -f "$state_file" ] && [ ! -f "$completion_file" ]; then
        echo "loki why: no run found here yet (no $state_file or $completion_file)." >&2
        echo "Run a build first: loki start <spec>" >&2
        return 1
    fi

    if [ "$as_json" = "1" ]; then
        local _why_json_head_sha
        _why_json_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
        _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
        _LOKI_WHY_HEAD_SHA="$_why_json_head_sha" \
        _LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" \
        _LOKI_WHY_ACTIONS_PY="$_LOKI_WHY_ACTIONS_PY" \
        _LOKI_WHY_EFFICIENCY="$loki_dir/metrics/efficiency" python3 - <<'WHYJSON'
import json, os
# Shared per-class action map (single definition; see _LOKI_WHY_ACTIONS_PY).
exec(os.environ.get("_LOKI_WHY_ACTIONS_PY", ""))
def load(p):
    try:
        with open(p) as f: return json.load(f)
    except Exception: return {}
state = load(os.environ.get("_LOKI_WHY_STATE", ""))
comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
last_error = load(os.environ.get("_LOKI_WHY_LAST_ERROR", ""))

# The human report labels a stale completion record "(from previous completed
# run)". The JSON consumer got the same record with NO such marker, so a script
# reading `completion.branch` or `completion.pr_url` after a crashed run would
# attribute a PREVIOUS run's branch and PR to the current one -- silently, and
# with no field to check. Emit the identical determination as explicit fields.
# Kept byte-identical in logic to the human path below; the two must not drift.
live_statuses = {"running", "exited"}
head_sha = os.environ.get("_LOKI_WHY_HEAD_SHA", "")
comp_head = comp.get("head_sha", "")
comp_is_stale = bool(state.get("status") in live_statuses) or bool(
    head_sha and comp_head and comp_head != head_sha
)
# Rework attribution, same source the human report and the completion summary
# read. Without it the JSON surface -- the one CI and automation consume -- knew
# only that a run hit its cap, not whether it was converging or thrashing. That
# is exactly the distinction the human output was taught to make, so omitting it
# here left the machine-readable path giving less than the printed one.
#
# Absent or unreadable records emit null rather than a zeroed object: a 0-of-0
# split would read as "no rework", a claim the data does not support.
_rework = None
_effdir = os.environ.get("_LOKI_WHY_EFFICIENCY", "")
if _effdir:
    try:
        import glob as _g
        _prog = _rew = 0
        _rcost = 0.0
        for _f in _g.glob(os.path.join(_effdir, "iteration-*.json")):
            try:
                _d = json.load(open(_f))
            except Exception:
                continue
            _st = str(_d.get("status", "")).lower()
            if _st in ("completed", "ok", "success"):
                _prog += 1
            elif _st in ("failed", "error", "blocked"):
                _rew += 1
                _rcost += float(_d.get("cost_usd") or 0)
        if _prog + _rew > 0:
            _rework = {
                "progress_iterations": _prog,
                "rework_iterations": _rew,
                "total_iterations": _prog + _rew,
                "rework_cost_usd": round(_rcost, 4),
                # The verdict a caller actually branches on, so every consumer
                # does not re-derive the threshold and drift from the human
                # report's wording.
                "verdict": "thrashing" if _rew * 2 >= _prog + _rew else "converging",
            }
    except Exception:
        _rework = None

# The machine-readable surface got the raw record and no action, so every
# consumer had to re-derive the remediation and drift from the printed report.
# Emit the SAME mapping the human path prints, plus an explicit honesty triple
# so a consumer can distinguish the three no-action cases instead of seeing one
# indistinguishable null:
#   present=False               -> no failure was recorded (NOT "it succeeded")
#   present=True, readable=False-> a record exists but is malformed
#   recognized=False            -> real record, class we have no action for
_le_path = os.environ.get("_LOKI_WHY_LAST_ERROR", "")
_le_present = bool(_le_path) and os.path.exists(_le_path)
_le_readable = isinstance(last_error, dict) and bool(last_error)
if _le_present and not _le_readable:
    # load() collapses unreadable and absent into {}. Re-read to tell them
    # apart -- reporting a malformed record as "no failure" is the exact lie
    # this command exists to prevent.
    try:
        with open(_le_path) as _lf:
            _parsed = json.load(_lf)
        _le_readable = isinstance(_parsed, dict)
    except Exception:
        _le_readable = False
_le_action = last_error_action(last_error, os.environ.get("LOKI_PROVIDER", "")) \
    if _le_readable else None

print(json.dumps({
    "state": state,
    "completion": comp,
    "last_error": last_error,
    "last_error_present": _le_present,
    "last_error_readable": _le_readable,
    "last_error_recognized": _le_action is not None,
    "last_error_action": _le_action,
    "completion_is_stale": comp_is_stale,
    "head_sha": head_sha or None,
    "rework": _rework,
}, indent=2))
WHYJSON
        return 0
    fi

    # Human-readable report. The diagnosis maps the terminal status to a plain
    # explanation + a concrete next action; everything is sourced from the files,
    # nothing is invented.
    local _why_head_sha
    _why_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
    _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
    _LOKI_WHY_HEAD_SHA="$_why_head_sha" \
    _LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" \
    _LOKI_WHY_UNCERTAINTY="$loki_dir/signals/UNCERTAINTY_ESCALATION" \
    _LOKI_WHY_EFFICIENCY="$loki_dir/metrics/efficiency" \
    _LOKI_WHY_CONVERGENCE="$loki_dir/council/convergence.log" \
    _LOKI_WHY_GATE="$loki_dir/signals/GATE_ESCALATION.json" \
    _LOKI_WHY_TARGET="${TARGET_DIR:-$(dirname "$loki_dir")}" \
    _LOKI_WHY_ACTIONS_PY="$_LOKI_WHY_ACTIONS_PY" \
    _LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
import json, os, glob
# Shared per-class action map (single definition; see _LOKI_WHY_ACTIONS_PY).
exec(os.environ.get("_LOKI_WHY_ACTIONS_PY", ""))
def load(p):
    try:
        with open(p) as f: return json.load(f)
    except Exception: return {}
state = load(os.environ.get("_LOKI_WHY_STATE", ""))
comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))

# completion.json uses a terminal-outcome vocabulary (complete / max_iterations /
# failed / stopped / force_stopped / intervention) that differs from the GUIDE
# keys below. Map it onto the GUIDE keys so the diagnosis is never "no mapping".
# The live state-file status (running / exited / paused / ...) is preferred when
# present; we only fall back to the (normalized) completion outcome.
COMP_OUTCOME_ALIASES = {
    "complete": "council_approved",
    "max_iterations": "max_iterations_reached",
    "intervention": "paused",
    # failed / stopped / force_stopped already match GUIDE keys verbatim.
}
status = state.get("status")
if not status:
    raw_outcome = comp.get("outcome") or ""
    status = COMP_OUTCOME_ALIASES.get(raw_outcome, raw_outcome) or "unknown"
exit_code = state.get("lastExitCode")
iters = state.get("iterationCount")

# status -> (one-line meaning, suggested next action). Honest + specific.
GUIDE = {
    "council_approved": ("The completion council agreed the work is done and verified.",
                         "Review the diff and open a PR (git push + gh pr create, or LOKI_AUTO_PR=1)."),
    "council_force_approved": ("Completion was force-approved (council could not fully converge).",
                               "Review the diff carefully before merging -- convergence was not unanimous."),
    "completion_promise_fulfilled": ("The agent declared its explicit completion promise fulfilled.",
                                     "Verify the promised outcome, then review and PR."),
    "max_iterations_reached": ("The build hit the iteration cap before the council approved it.",
                               "Inspect what is left (loki status), raise LOKI_MAX_ITERATIONS or narrow the spec, and resume."),
    "max_retries_exceeded": ("The build exhausted its retry budget on a repeating failure.",
                             "Read .loki/logs for the recurring error (rate limit? failing test?), fix the root cause, then re-run."),
    "failed": ("The build ended in a failure state.",
               "Read .loki/logs/ + the handoff below for the failure, fix it, then re-run."),
    "policy_blocked": ("A policy/trust gate blocked completion.",
                       "Review the blocking finding; address it or use the documented override path."),
    "budget_exceeded": ("The cost budget breaker paused the build.",
                        "Raise LOKI_BUDGET_LIMIT or accept the partial result, then resume."),
    "paused": ("The build is paused (human-intervention signal).",
               "Resume with: loki resume."),
    "interrupted": ("The build was interrupted before a terminal state.",
                    "Resume with: loki resume."),
    "stopped": ("The build was stopped by the operator.",
                "Start a new build with loki start, or resume if you meant to continue."),
    "force_stopped": ("The build was force-stopped.",
                      "Start a new build, or investigate why a force-stop was needed."),
    "running": ("The recorded state says a build is still running (or crashed mid-run).",
                "If no build is active it likely crashed; in durable mode (LOKI_DURABLE_STATE=1) a restart resumes, else loki start re-runs."),
    "exited": ("The build process exited mid-iteration (likely a crash, kill, or empty provider output).",
               "Read .loki/logs/ for the last error; if nothing is running it crashed -- re-run with loki start, or loki resume in durable mode."),
    # A real terminal state that had no entry, so `loki why` answered the one
    # question it exists to answer with "No diagnosis mapping for this status".
    "inconclusive_spec_contradiction": (
        "Loki could not reconcile parts of the spec, so it stopped rather than guess.",
        "The specific contradictions are in .loki/assumptions/ledger.md -- resolve them in the spec, then re-run."),
}
meaning, action = GUIDE.get(status, ("No diagnosis mapping for this status; see the raw fields below.",
                                     "Check loki status and .loki/logs/ for detail."))

# Rework-aware advice for the iteration cap. The static line said "raise
# LOKI_MAX_ITERATIONS", which is right for a run that was CONVERGING and wrong
# for one that was THRASHING -- there, a higher cap buys more thrashing at full
# price. The completion summary already splits those cases; this command is
# where v8.14.0 sends the user, so it must not contradict it.
#
# Reads the same attribution the summary and the prompt trend use, so all three
# surfaces cannot disagree about one run. Falls back to the static line when
# the split is unknown: guessing which case applies would be worse than the
# generic advice.
if status == "max_iterations_reached":
    try:
        import glob as _glob
        _prog = _rew = 0
        _rcost = 0.0
        _effdir = os.environ.get("_LOKI_WHY_EFFICIENCY", "")
        for _f in (_glob.glob(os.path.join(_effdir, "iteration-*.json")) if _effdir else []):
            try:
                _d = json.load(open(_f))
            except Exception:
                continue
            _st = str(_d.get("status", "")).lower()
            if _st in ("completed", "ok", "success"):
                _prog += 1
            elif _st in ("failed", "error", "blocked"):
                _rew += 1
                _rcost += float(_d.get("cost_usd") or 0)
        if _prog + _rew > 0:
            _tot = _prog + _rew
            if _rew * 2 >= _tot:
                action = (
                    f"{_rew} of {_tot} iterations redid earlier work"
                    + (f" (${_rcost:.2f})" if _rcost > 0 else "")
                    + ". Raising LOKI_MAX_ITERATIONS buys more of the same at full price. "
                      "Read the last reviewer findings in .loki/quality/reviews/ and fix the "
                      "blocker, or narrow the spec."
                )
            else:
                action = (
                    f"Converging: {_prog} of {_tot} iterations made progress"
                    + (f", {_rew} redid work (${_rcost:.2f})" if _rew else "")
                    + ". Raise LOKI_MAX_ITERATIONS and resume."
                )
    except Exception:
        pass

# The completion.json branch/changes/PR belong to the LAST COMPLETED run. When
# the live state shows a build that is still running or crashed mid-iteration
# (running/exited), or when the completion record's head_sha no longer matches
# the current git HEAD, those fields describe a PREVIOUS run -- not this one.
# Label them honestly so a crashed run is never reported with a stale PR/branch.
live_statuses = {"running", "exited"}
head_sha = os.environ.get("_LOKI_WHY_HEAD_SHA", "")
comp_head = comp.get("head_sha", "")
comp_is_stale = bool(state.get("status") in live_statuses) or bool(
    head_sha and comp_head and comp_head != head_sha
)
comp_label = "  (from previous completed run)" if comp_is_stale else ""

print("Loki: why")
print("=" * 60)
print(f"  Outcome     : {status}")
if exit_code is not None:
    print(f"  Exit code   : {exit_code}")
if iters is not None:
    print(f"  Iterations  : {iters}")
if comp.get("branch"):
    print(f"  Branch      : {comp['branch']}{comp_label}")
_fc = comp.get("files_changed")
if _fc is not None:
    if _fc == 0:
        # A recorded 0 is NOT trustworthy on its own: a greenfield run whose
        # baseline never resolved records 0 while having committed real work.
        # Saying "0 files" flatly told users nothing was built on a run that
        # produced 9 files and 1300 insertions. Re-derive from git, and only
        # claim zero when git agrees.
        _real = None
        try:
            import subprocess as _sp
            _tgt = os.environ.get("_LOKI_WHY_TARGET") or "."
            _base = (comp.get("start_sha") or "").strip()
            if not _base or _base == "HEAD":
                _base = _sp.run(["git", "-C", _tgt, "hash-object", "-t", "tree", os.devnull],
                                capture_output=True, text=True, timeout=10).stdout.strip()
            if _base:
                _o = _sp.run(["git", "-C", _tgt, "diff", "--shortstat", f"{_base}..HEAD",
                              "--", ".", ":(exclude).loki/"],
                             capture_output=True, text=True, timeout=15).stdout.strip()
                if _o:
                    _real = _o
        except Exception:
            _real = None
        if _real:
            print(f"  Changes     : {_real}  (re-derived; the run record said 0)")
        else:
            print(f"  Changes     : none recorded (the run did not resolve a baseline)")
    else:
        print(f"  Changes     : {_fc} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)}){comp_label}")
if comp.get("pr_url"):
    print(f"  PR          : {comp['pr_url']}{comp_label}")

# Name the gate that actually stopped the run. Without this the user is told to
# "resume", which the run's own pause notice warns will stop at the same gate.
_gate = load(os.environ.get("_LOKI_WHY_GATE", "")) or {}
_gname = str(_gate.get("gate", "") or "").strip()
if _gname:
    _gc, _gt = _gate.get("count"), _gate.get("threshold")
    _detail = f"{_gname} (failed {_gc} times, threshold {_gt})" if isinstance(_gc, int) and isinstance(_gt, int) else _gname
    print(f"  Blocked by  : {_detail}")

print()
print(f"  What happened: {meaning}")
if _gname:
    print(f"  What to do   : Read the {_gname} findings and fix what they name, then resume.")
    print(f"                 Resuming unchanged will stop at the same gate again.")
    _art = str(_gate.get("latest_artifact", "") or "")
    if _art:
        # Make the pointer relative to the workspace. The recorded value is an
        # absolute path from the machine that ran the build -- on the preserved
        # run it names a /var/folders temp dir that no longer exists, so the one
        # actionable pointer in the whole notice led nowhere. Anchor on
        # ".loki/" so it works even when the run happened elsewhere.
        _i = _art.find(".loki/")
        if _i != -1:
            _art = _art[_i:]
        print(f"                 Findings: {_art}")
else:
    print(f"  What to do   : {action}")

# RUN-25 iter 22b/iter 23 (Wave D #6): when a run is PAUSED / live and actually
# STUCK, name the REAL stall reason instead of the generic "resume". The runner
# drops .loki/signals/UNCERTAINTY_ESCALATION when its proactive stuck-detector
# fires (repeated no-progress / uncertainty), and council/convergence.log records
# consecutive-no-change. Surface those so "why is it stuck" has a real answer, not
# a generic resume hint. Reads existing files only (no new state, no spend).
_unc = os.environ.get("_LOKI_WHY_UNCERTAINTY", "")
if _unc and os.path.exists(_unc):
    print()
    print("  Stall reason : the runner's proactive stuck-detector escalated")
    print("                 (UNCERTAINTY_ESCALATION): repeated no-progress or")
    print("                 low-confidence iterations. It is not making headway on its own.")
    # Add the consecutive-no-change count from the convergence log tail if present.
    _conv = os.environ.get("_LOKI_WHY_CONVERGENCE", "")
    if _conv and os.path.exists(_conv):
        try:
            with open(_conv) as _cf:
                _tail = _cf.read().splitlines()[-1:]
            if _tail:
                print(f"                 Latest convergence signal: {_tail[0][:120]}")
        except Exception:
            pass
    print("  Unstick it   : give the loop a concrete next step:")
    print("                   loki steer \"<what to try next>\"   (Loki 10 engine; legacy loop needs LOKI_PROMPT_INJECTION=true)")
    print("                 or narrow the spec / fix the blocker, then loki resume.")

# Surface the structured per-iteration error record, if one exists at
# .loki/state/LAST_ERROR.json (the run.sh writer that produces it ships in the
# SAME v7.92.0 release; on an older engine the file is simply absent and this
# block stays dormant - never fabricated). run.sh clears it at the start of every
# fresh run, so a present record belongs to the CURRENT run. We still only show it
# when THIS run's outcome is itself a failure - never beside a success verdict
# (that would be a fake-green-adjacent lie) - using the same SUCCESS set below.
SUCCESS_STATUSES = {"council_approved", "completion_promise_fulfilled", "complete", "completed"}
le = {}
_le_path = os.environ.get("_LOKI_WHY_LAST_ERROR", "")
_le_bad = ""
try:
    with open(_le_path) as f:
        le = json.load(f)
    if not isinstance(le, dict):
        # A bare list/string/null is not a record. Matches the isinstance guard
        # below rather than silently rendering an empty one.
        _le_bad = "record is a %s, expected a JSON object" % type(le).__name__
        le = {}
except IOError:
    le = {}          # absent: genuinely no recorded failure. Say nothing here.
except ValueError as _e:
    # Present but unparseable. Reporting silence here would read as "no failure
    # recorded", which is a different and false claim.
    le = {}
    _le_bad = str(_e)
if _le_bad and status not in SUCCESS_STATUSES:
    print()
    print(f"  Last error  : a failure record exists but could not be read.")
    print(f"                File  : {_le_path}")
    print(f"                Reason: {_le_bad}")
    print(f"                Nothing is inferred from an unreadable record.")
if isinstance(le, dict) and le.get("error_class") and status not in SUCCESS_STATUSES:
    print()
    _it = le.get("iteration")
    _it_s = f" (iteration {_it})" if _it not in (None, "") else ""
    print(f"  Last error  : {le.get('error_class')}{_it_s}")
    if le.get("brief"):
        print(f"                {le.get('brief')}")
    # Per-class remediation. Naming the error_class told the user WHAT broke and
    # nothing about what to do; the GUIDE map above is keyed on run STATUS, so a
    # failed run got the same generic "read the logs" line whether the cause was
    # a 401 or a timeout. This maps the CLASS to its own concrete action.
    #
    # Kept in lockstep with LAST_ERROR_ACTIONS in the --json path above -- the
    # two must not drift (same norm as the GUIDE / _loki_next_action pair).
    # run.sh classifies the error but carries NO remediation wording, so this is
    # the sole home for these strings.
    _act = last_error_action(le, os.environ.get("LOKI_PROVIDER", ""))
    if _act:
        for _i, _line in enumerate(_act.splitlines()):
            print(f"  {'What to do  :' if _i == 0 else '             '} {_line}")
    else:
        # Unrecognized class: the brief above is all we honestly know. Say that
        # rather than emitting a generic action that implies a diagnosis.
        print("                (Unrecognized error class -- the description above is "
              "the recorded text, verbatim. No cause is inferred.)")

# Surface the latest structured handoff (already-captured context), honestly.
hd = sorted(glob.glob(os.path.join(os.environ.get("_LOKI_WHY_HANDOFFS",""), "*.md")))
if hd:
    print()
    print(f"  Latest handoff: {hd[-1]}")
    try:
        with open(hd[-1]) as f:
            head = "".join(f.readlines()[:8]).rstrip()
        for line in head.splitlines():
            print(f"    {line}")
    except Exception:
        pass
print()
print("  (loki why --json for the raw record; loki status for live state.)")
WHYTXT
    return 0
}

# Show current status
cmd_status() {
    # Check for flags
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --json) cmd_status_json; return $? ;;
            --badge) echo "loki status --badge requires the Bun CLI (loki-ts); not available on the bash fallback" >&2; return 1 ;;
            --probe) echo "loki status --probe requires the Bun CLI (loki-ts); not available on the bash fallback" >&2; return 1 ;;
            --help|-h) echo "Usage: loki status [--json|--badge|--probe]"; return 0 ;;
            *)
                echo -e "${RED}Unknown flag: $1${NC}"
                echo "Usage: loki status [--json|--badge|--probe]"
                return 1
                ;;
        esac
    done

    require_jq || return 1

    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${BOLD}Loki Mode Status${NC}"
        echo ""
        echo -e "${YELLOW}No active session found.${NC}"
        echo "Loki Mode has not been initialized in this directory."
        echo ""
        echo "To start a session:"
        echo "  loki start <prd>              - Start with a PRD file"
        echo "  loki start                    - Start without a PRD"
        echo ""
        echo -e "${DIM}Current directory: $(pwd)${NC}"
        return 0
    fi

    echo -e "${BOLD}Loki Mode Status${NC}"
    echo ""

    # Show current provider
    local saved_provider=""
    if [ -f "$LOKI_DIR/state/provider" ]; then
        saved_provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
    fi
    local current_provider="${saved_provider:-${LOKI_PROVIDER:-claude}}"
    # Show provider with capability
    local capability="full features"
    case "$current_provider" in
        codex|aider)
            capability="degraded mode"
            ;;
        cline)
            capability="near-full mode"
            ;;
        opencode)
            capability="model-agnostic mode"
            ;;
    esac
    echo -e "${CYAN}Provider:${NC} $current_provider ($capability)"
    echo -e "${DIM}  Switch with: loki provider set <claude|codex|cline|aider|opencode>${NC}"
    echo ""

    # Show running sessions (v6.4.0 - concurrent session support)
    local running_sessions=()
    while IFS= read -r line; do
        [ -n "$line" ] && running_sessions+=("$line")
    done < <(list_running_sessions 2>/dev/null)

    if [ ${#running_sessions[@]} -gt 0 ]; then
        echo -e "${GREEN}Active Sessions: ${#running_sessions[@]}${NC}"
        for entry in "${running_sessions[@]}"; do
            local sid="${entry%%:*}"
            local spid="${entry#*:}"
            if [ "$sid" = "global" ]; then
                echo -e "  ${CYAN}[global]${NC} PID $spid"
            else
                echo -e "  ${CYAN}[#$sid]${NC} PID $spid"
            fi
        done
        echo ""
        echo -e "${DIM}  Stop specific: loki stop <session-id>${NC}"
        echo -e "${DIM}  Stop all:      loki stop${NC}"
        echo ""
    fi

    # Check for signals first (more prominent)
    if [ -f "$LOKI_DIR/PAUSE" ]; then
        echo -e "${YELLOW}Status: PAUSED${NC}"
        echo -e "${DIM}  Resume with: loki resume${NC}"
        echo ""
    elif [ -f "$LOKI_DIR/STOP" ]; then
        echo -e "${RED}Status: STOPPED${NC}"
        echo -e "${DIM}  Clear with: loki resume${NC}"
        echo ""
    elif _loki_print_interrupted_resume_hint; then
        # An interrupted run (Ctrl-C / crash / closed laptop) leaves no signal
        # file, so the two branches above miss it entirely. Same hint cmd_resume
        # prints. elif, not a separate block: it is mutually exclusive with an
        # explicit PAUSE/STOP, and must not stack onto the "Last run:" line.
        echo ""
    fi

    # Check status file
    if [ -f "$LOKI_DIR/STATUS.txt" ]; then
        echo -e "${CYAN}Session Info:${NC}"
        cat "$LOKI_DIR/STATUS.txt"
        echo ""
    fi

    # Check orchestrator state
    if [ -f "$LOKI_DIR/state/orchestrator.json" ]; then
        echo -e "${CYAN}Orchestrator State:${NC}"
        local orch_phase
        # An empty orchestrator.json makes jq exit 0 with no output, so the
        # `|| echo unknown` fallback never fires and the phase line is blank.
        # Capture and normalize an empty result to "unknown".
        orch_phase=$(jq -r '.currentPhase // "unknown"' "$LOKI_DIR/state/orchestrator.json" 2>/dev/null || echo "unknown")
        [ -n "$orch_phase" ] || orch_phase="unknown"
        echo "$orch_phase"
    fi

    # Check pending tasks
    if [ -f "$LOKI_DIR/queue/pending.json" ]; then
        local task_count
        task_count=$(jq 'if type == "array" then length elif .tasks then .tasks | length else 0 end' "$LOKI_DIR/queue/pending.json" 2>/dev/null || echo "0")
        # An empty or whitespace-only pending.json makes jq exit 0 while
        # printing nothing, so the `|| echo 0` fallback never fires and the
        # status line renders a blank count. Normalize any non-numeric result
        # (empty string, null, parse noise) back to 0.
        case "$task_count" in
            ''|*[!0-9]*) task_count=0 ;;
        esac
        echo -e "${CYAN}Pending Tasks:${NC} $task_count"
    fi

    # Check budget with visual gauge
    if [ -f "$LOKI_DIR/metrics/budget.json" ]; then
        local budget_limit budget_used budget_remaining
        budget_limit=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
        budget_used=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); v=d.get('budget_used'); print('unmeasured' if v is None else round(v, 2))" 2>/dev/null || echo "0")
        if [ "$budget_used" = "unmeasured" ]; then
            if [ "$budget_limit" != "0" ]; then
                echo -e "${CYAN}Budget:${NC} unmeasured / \$$budget_limit"
            else
                echo -e "${CYAN}Cost:${NC} unmeasured"
            fi
        elif [ "$budget_limit" != "0" ]; then
            echo -e "${CYAN}Budget:${NC} \$$budget_used / \$$budget_limit"
            # Show budget gauge if TUI is loaded
            if type context_gauge &>/dev/null; then
                local used_cents remaining_cents limit_cents
                used_cents=$(echo "scale=0; $budget_used * 100" | bc 2>/dev/null || echo "0")
                limit_cents=$(echo "scale=0; $budget_limit * 100" | bc 2>/dev/null || echo "100")
                # Render as integer cents for the gauge
                used_cents=${used_cents%.*}
                limit_cents=${limit_cents%.*}
                [ -z "$used_cents" ] && used_cents=0
                [ -z "$limit_cents" ] && limit_cents=100
                context_gauge "$used_cents" "$limit_cents" "Budget"
            fi
        else
            echo -e "${CYAN}Cost:${NC} \$$budget_used (no limit)"
        fi
    fi

    # Context window usage (token tracking)
    if [ -f "$LOKI_DIR/state/context-usage.json" ]; then
        local ctx_used ctx_total
        ctx_total=$(_LOKI_CTX_FILE="$LOKI_DIR/state/context-usage.json" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
        ctx_used=$(_LOKI_CTX_FILE="$LOKI_DIR/state/context-usage.json" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
        if type context_gauge &>/dev/null; then
            context_gauge "$ctx_used" "$ctx_total" "Context"
        else
            local ctx_pct=0
            if [ "$ctx_total" -gt 0 ] 2>/dev/null; then
                ctx_pct=$((ctx_used * 100 / ctx_total))
            fi
            echo -e "${CYAN}Context:${NC} ${ctx_pct}% (${ctx_used} / ${ctx_total} tokens)"
        fi
    fi

    # Check dashboard
    if [ -f "$LOKI_DIR/dashboard/dashboard.pid" ]; then
        local pid=$(cat "$LOKI_DIR/dashboard/dashboard.pid" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            local port=${LOKI_DASHBOARD_PORT:-57374}
            echo -e "${CYAN}Dashboard:${NC} http://127.0.0.1:$port/"
        fi
    fi

    # P12-19: freshness of the newest Loki 10 run, same wording as the Bun route
    # ("last event Ns ago (seq N)"). Display-only; silent when there is no run or no parseable event.
    local _e10_last _e10_ev _e10_seq _e10_ts _e10_epoch
    _e10_last=$(ls -d "$LOKI_DIR"/runs/e10-*/ 2>/dev/null | sort | tail -1) || _e10_last=""
    _e10_ev="${_e10_last%/}/events.jsonl"
    if [ -n "$_e10_last" ] && [ -s "$_e10_ev" ]; then
        _e10_seq=$(tail -n 1 "$_e10_ev" | jq -r '.seq // empty' 2>/dev/null) || _e10_seq=""
        _e10_ts=$(tail -n 1 "$_e10_ev" | jq -r '.ts // empty' 2>/dev/null) || _e10_ts=""
        _e10_epoch=$(python3 -c "import sys,datetime; print(int(datetime.datetime.fromisoformat(sys.argv[1].replace('Z','+00:00')).timestamp()))" "$_e10_ts" 2>/dev/null) || _e10_epoch=""
        if [ -n "$_e10_seq" ] && [ -n "$_e10_epoch" ]; then
            local _e10_age=$(( $(date +%s) - _e10_epoch ))
            [ "$_e10_age" -lt 0 ] && _e10_age=0
            echo -e "${CYAN}Freshness:${NC} last event ${_e10_age}s ago (seq ${_e10_seq})"
        fi
    fi

    # v7.82: surface the single most valuable, most shareable fact -- where the
    # built app is running -- when the app runner reports a live app. Display-only
    # read of .loki/app-runner/state.json (same shape build_completion_summary
    # reads); silent when no app is running. The --json path (cmd_status_json) is
    # untouched, so machine output stays byte-identical.
    if [ -f "$LOKI_DIR/app-runner/state.json" ]; then
        local app_url
        app_url=$(_LOKI_APP_STATE="$LOKI_DIR/app-runner/state.json" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['_LOKI_APP_STATE']))
    print(d.get('url', '') if d.get('status') == 'running' else '')
except Exception:
    print('')" 2>/dev/null)
        if [ -n "$app_url" ]; then
            echo ""
            echo -e "${GREEN}App running:${NC} ${BOLD}${app_url}${NC}  ${DIM}- open it to try it${NC}"
        fi
    fi

    # 133-F4: newest sealed receipt's verdict, read from the receipt field only
    # (never recomputed). Missing, empty or unparseable receipts never read as
    # VERIFIED. This replaces the old completion.json one-liner (Bun never had it).
    local receipt_line
    receipt_line=$(_LOKI_RUNS="$LOKI_DIR/runs" python3 -c "
import json, os, re
runs = os.environ['_LOKI_RUNS']
try:
    ids = sorted(d for d in os.listdir(runs) if os.path.isdir(os.path.join(runs, d)))
except Exception:
    ids = []
if not ids:
    print('Last run: no receipt (verdict NOT AVAILABLE)')
else:
    try:
        d = json.load(open(os.path.join(runs, ids[-1], 'receipt.json')))
        v = d['verdict']
        h = d['receipt_sha256']
        if not (isinstance(v, str) and re.fullmatch(r'[A-Z_]+', v) and isinstance(h, str)):
            raise ValueError
        h = h[7:] if h.startswith('sha256:') else h
        if not re.fullmatch(r'[0-9a-f]{12,}', h):
            raise ValueError
        print('Last run: %s (receipt sha256:%s)' % (v, h[:12]))
    except Exception:
        print('Last run: receipt unreadable')
" 2>/dev/null)
    [ -n "$receipt_line" ] || receipt_line="Last run: receipt unreadable"
    echo -e "${DIM}${receipt_line}${NC}"

    echo ""
    echo -e "${DIM}  Tip: loki analyze context show   - detailed token breakdown${NC}"
    echo -e "${DIM}  Tip: loki analyze code overview  - codebase intelligence${NC}"
}

# JSON output for loki status --json
cmd_status_json() {
    local skill_dir="$SKILL_DIR"
    local loki_dir="$LOKI_DIR"
    local dashboard_port="${LOKI_DASHBOARD_PORT:-57374}"
    local env_provider="${LOKI_PROVIDER:-claude}"

    if ! python3 -c "
import json, os, sys, time

skill_dir = sys.argv[1]
loki_dir = sys.argv[2]
dashboard_port = sys.argv[3]
env_provider = sys.argv[4]
result = {}

# UT2-13: Helper -- read cli-provider marker written by --provider flag dispatch.
# v7.7.11 (council fix): file format is now '<provider>:<unix_ts>:<pid>' so we
# can drop the marker when the originating process is gone (SIGKILL / crash
# paths that bypassed our cleanup hooks). Also validates provider name against
# canonical set so a typo writing the file can't pollute status output.
# Legacy format '<provider>:<unix_ts>' still parses (no PID liveness check).
# Returns (provider_name, True) when valid; (None, False) otherwise.
def _read_cli_provider(loki_dir):
    cli_file = os.path.join(loki_dir, 'state', 'cli-provider')
    if not os.path.isfile(cli_file):
        return None, False
    try:
        content = open(cli_file).read().strip()
        parts = content.split(':')
        if len(parts) < 2:
            return None, False
        prov = parts[0]
        ts = int(parts[1])
        if prov not in ('claude', 'codex', 'cline', 'aider', 'opencode'):
            return None, False
        if time.time() - ts > 86400:
            return None, False
        # If a PID is present, drop the marker when that process is gone.
        if len(parts) >= 3:
            try:
                pid = int(parts[2])
                if pid > 0:
                    os.kill(pid, 0)
            except (ValueError, ProcessLookupError, PermissionError):
                # PID missing/dead/cross-uid -> treat as stale.
                return None, False
        return prov, True
    except Exception:
        return None, False

# Version
version_file = os.path.join(skill_dir, 'VERSION')
if os.path.isfile(version_file):
    with open(version_file) as f:
        result['version'] = f.read().strip()
else:
    result['version'] = 'unknown'

# Check if session exists
if not os.path.isdir(loki_dir):
    result['status'] = 'inactive'
    result['phase'] = None
    result['iteration'] = 0
    # UT2-13: CLI check first (inactive path -- loki_dir may not exist yet).
    _cli_prov, _cli_set = _read_cli_provider(loki_dir)
    if _cli_set:
        result['provider'] = _cli_prov
        result['provider_source'] = 'cli'
    elif os.environ.get('LOKI_PROVIDER', ''):
        result['provider'] = env_provider
        result['provider_source'] = 'env'
    else:
        result['provider'] = 'claude'
        result['provider_source'] = 'default'
    result['dashboard_url'] = None
    result['pid'] = None
    result['elapsed_time'] = 0
    result['task_counts'] = {'total': 0, 'completed': 0, 'failed': 0, 'pending': 0}
    result['phase1'] = {
        'findings_iters': 0,
        'learnings_count': 0,
        'escalations_count': 0,
        'pause_signal': False,
        'gate_failure_counts': {},
    }
    print(json.dumps(result, indent=2))
    sys.exit(0)

# Status from signals and session.json
if os.path.isfile(os.path.join(loki_dir, 'PAUSE')):
    result['status'] = 'paused'
elif os.path.isfile(os.path.join(loki_dir, 'STOP')):
    result['status'] = 'stopped'
else:
    session_file = os.path.join(loki_dir, 'session.json')
    if os.path.isfile(session_file):
        try:
            with open(session_file) as f:
                session = json.load(f)
            result['status'] = session.get('status', 'unknown')
        except Exception:
            result['status'] = 'unknown'
    else:
        result['status'] = 'unknown'

# Phase and iteration from dashboard-state.json
ds_file = os.path.join(loki_dir, 'dashboard-state.json')
if os.path.isfile(ds_file):
    try:
        with open(ds_file) as f:
            ds = json.load(f)
        result['phase'] = ds.get('phase', ds.get('currentPhase'))
        result['iteration'] = ds.get('iteration', ds.get('currentIteration', 0))
    except Exception:
        result['phase'] = None
        result['iteration'] = 0
else:
    orch_file = os.path.join(loki_dir, 'state', 'orchestrator.json')
    if os.path.isfile(orch_file):
        try:
            with open(orch_file) as f:
                orch = json.load(f)
            result['phase'] = orch.get('currentPhase')
            result['iteration'] = orch.get('currentIteration', 0)
        except Exception:
            result['phase'] = None
            result['iteration'] = 0
    else:
        result['phase'] = None
        result['iteration'] = 0

# Provider + provider_source (v7.7.2 B-5 clarity: surface WHY this
# value won the resolution race: 'cli' > 'saved' > 'env' > 'default').
# UT2-13: cli source is checked FIRST -- overrides saved/env/default.
_cli_prov, _cli_set = _read_cli_provider(loki_dir)
provider_file = os.path.join(loki_dir, 'state', 'provider')
if os.path.isfile(provider_file):
    try:
        with open(provider_file) as f:
            saved = f.read().strip()
    except OSError:
        saved = ''
else:
    saved = ''
env_set = os.environ.get('LOKI_PROVIDER', '') != ''
if _cli_set:
    result['provider'] = _cli_prov
    result['provider_source'] = 'cli'
elif saved:
    result['provider'] = saved
    result['provider_source'] = 'saved'
elif env_set:
    result['provider'] = env_provider
    result['provider_source'] = 'env'
else:
    result['provider'] = 'claude'
    result['provider_source'] = 'default'

# PID
pid_file = os.path.join(loki_dir, 'loki.pid')
if os.path.isfile(pid_file):
    try:
        with open(pid_file) as f:
            result['pid'] = int(f.read().strip())
    except (ValueError, Exception):
        result['pid'] = None
else:
    result['pid'] = None

# Elapsed time from session.json
session_file = os.path.join(loki_dir, 'session.json')
if os.path.isfile(session_file):
    try:
        with open(session_file) as f:
            session = json.load(f)
        start_time = session.get('start_time', session.get('startTime'))
        if start_time:
            if isinstance(start_time, (int, float)):
                result['elapsed_time'] = int(time.time() - start_time)
            else:
                from datetime import datetime
                dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
                result['elapsed_time'] = int(time.time() - dt.timestamp())
        else:
            result['elapsed_time'] = 0
    except Exception:
        result['elapsed_time'] = 0
else:
    result['elapsed_time'] = 0

# Dashboard URL. Check both the project-local in-build dashboard and the
# standalone dashboard (~/.loki/dashboard) and honor saved scheme/host/port
# side-files, so --json reports the right URL regardless of which started it.
_dash_candidates = [
    os.path.join(loki_dir, 'dashboard', 'dashboard.pid'),
    os.path.expanduser(os.path.join('~', '.loki', 'dashboard', 'dashboard.pid')),
]
dashboard_pid_file = next((p for p in _dash_candidates if os.path.isfile(p)), _dash_candidates[0])
dashboard_url = None
if os.path.isfile(dashboard_pid_file):
    try:
        with open(dashboard_pid_file) as f:
            dpid = int(f.read().strip())
        os.kill(dpid, 0)
        _dd = os.path.dirname(dashboard_pid_file)
        def _side(name, default):
            p = os.path.join(_dd, name)
            try:
                return open(p).read().strip() if os.path.isfile(p) else default
            except OSError:
                return default
        _scheme = _side('scheme', 'http')
        _host = _side('host', '127.0.0.1')
        _port = _side('port', str(dashboard_port))
        if _host == '0.0.0.0':
            _host = '127.0.0.1'
        dashboard_url = _scheme + '://' + _host + ':' + _port + '/'
    except (ProcessLookupError, PermissionError, ValueError, Exception):
        pass
result['dashboard_url'] = dashboard_url

# Task counts from queue files
task_counts = {'total': 0, 'completed': 0, 'failed': 0, 'pending': 0}
queue_dir = os.path.join(loki_dir, 'queue')
if os.path.isdir(queue_dir):
    for name, key in [('pending.json', 'pending'), ('completed.json', 'completed'), ('failed.json', 'failed')]:
        fpath = os.path.join(queue_dir, name)
        if os.path.isfile(fpath):
            try:
                with open(fpath) as f:
                    data = json.load(f)
                if isinstance(data, list):
                    task_counts[key] = len(data)
                elif isinstance(data, dict) and 'tasks' in data:
                    task_counts[key] = len(data['tasks'])
            except Exception:
                pass
    task_counts['total'] = task_counts['pending'] + task_counts['completed'] + task_counts['failed']
result['task_counts'] = task_counts

# v7.5.5 (#204): Phase 1 (RARV-C closure) artifact summary -- mirror of the
# Bun route in loki-ts/src/commands/status.ts STATUS_JSON_PY. Keep in sync.
phase1 = {
    'findings_iters': 0,
    'learnings_count': 0,
    'escalations_count': 0,
    'pause_signal': False,
    'gate_failure_counts': {},
}
state_dir = os.path.join(loki_dir, 'state')
if os.path.isdir(state_dir):
    try:
        phase1['findings_iters'] = sum(
            1 for n in os.listdir(state_dir)
            if n.startswith('findings-') and n.endswith('.json')
        )
    except Exception:
        pass
    learnings_file = os.path.join(state_dir, 'relevant-learnings.json')
    if os.path.isfile(learnings_file):
        try:
            with open(learnings_file) as f:
                learnings = json.load(f)
            if isinstance(learnings, list):
                phase1['learnings_count'] = len(learnings)
            elif isinstance(learnings, dict):
                entries = learnings.get('entries')
                if isinstance(entries, list):
                    phase1['learnings_count'] = len(entries)
        except Exception:
            pass
escalations_dir = os.path.join(loki_dir, 'escalations')
if os.path.isdir(escalations_dir):
    try:
        phase1['escalations_count'] = sum(
            1 for n in os.listdir(escalations_dir) if n.endswith('.md')
        )
    except Exception:
        pass
phase1['pause_signal'] = os.path.isfile(os.path.join(loki_dir, 'PAUSE'))
gate_count_file = os.path.join(loki_dir, 'quality', 'gate-failure-count.json')
if os.path.isfile(gate_count_file):
    try:
        with open(gate_count_file) as f:
            gc = json.load(f)
        if isinstance(gc, dict):
            phase1['gate_failure_counts'] = {
                k: v for k, v in gc.items() if isinstance(v, (int, float))
            }
    except Exception:
        pass
result['phase1'] = phase1

print(json.dumps(result, indent=2))
" "$skill_dir" "$loki_dir" "$dashboard_port" "$env_provider"; then
        # WAVE8 loki-F2: under `set -euo pipefail` a bare python3 call aborts
        # the whole function on non-zero exit, so the old post-call
        # `if [ $? -ne 0 ]` fallback was DEAD code -- a missing/broken python3
        # crashed `loki status --json` instead of degrading. Guarding the call
        # with `if ! ...; then` catches the non-zero exit and emits the honest
        # error object. (Most malformed state files already degrade internally
        # via per-file try/except; this covers the interpreter-failure case.)
        echo '{"error": "Failed to generate JSON status. Ensure python3 is available."}' >&2
        return 1
    fi
}

# Session statistics
cmd_stats() {
    # v7.6.2 B-13 fix: --help must print help, not render the stats panel.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- session statistics${NC}"
            echo ""
            echo "Usage: loki stats [options]"
            echo ""
            echo "Prints session-level statistics: tasks completed, iterations,"
            echo "tokens used, cost, council votes, gate verdicts."
            echo ""
            echo "Options:"
            echo "  --json          JSON output (machine-readable)"
            echo "  --efficiency    Include cost/token efficiency breakdown"
            echo "  --help, -h      Show this help and exit"
            echo ""
            echo "See also: loki kpis, loki status"
            return 0
            ;;
    esac
    local show_json=false
    local show_efficiency=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --json) show_json=true; shift ;;
            --efficiency) show_efficiency=true; shift ;;
            *) echo "Unknown option: $1 (see: loki stats --help)" >&2; return 2 ;;
        esac
    done

    if [ ! -d "$LOKI_DIR" ]; then
        if [ "$show_json" = true ]; then
            echo '{"error": "No active session"}'
        else
            echo -e "${YELLOW}No active session found.${NC}"
            echo "Start a session with: loki start <prd>"
        fi
        return 0
    fi

    local loki_dir="$LOKI_DIR"
    local eff_flag="$show_efficiency"
    local json_flag="$show_json"

    python3 -c "
import json, os, sys, glob

loki_dir = sys.argv[1]
show_efficiency = sys.argv[2] == 'true'
show_json = sys.argv[3] == 'true'

# --- Gather data ---

# Session state (orchestrator)
phase = 'N/A'
iteration_count = 0
orch_file = os.path.join(loki_dir, 'state', 'orchestrator.json')
if os.path.isfile(orch_file):
    try:
        with open(orch_file) as f:
            orch = json.load(f)
        phase = orch.get('currentPhase', 'N/A')
        iteration_count = orch.get('currentIteration', 0)
    except Exception:
        pass

# Per-iteration metrics
eff_dir = os.path.join(loki_dir, 'metrics', 'efficiency')
iterations = []
if os.path.isdir(eff_dir):
    for path in sorted(glob.glob(os.path.join(eff_dir, 'iteration-*.json'))):
        try:
            with open(path) as f:
                iterations.append(json.load(f))
        except Exception:
            pass

# If we have iteration files, use their count (more accurate)
if iterations:
    iteration_count = max(iteration_count, len(iterations))

total_input = sum(it.get('input_tokens', 0) for it in iterations)
total_output = sum(it.get('output_tokens', 0) for it in iterations)
total_tokens = total_input + total_output
total_cost = sum(it.get('cost_usd', 0) for it in iterations)
cost_measured = any(it.get('cost_usd') is not None for it in iterations)
total_duration = sum(it.get('duration_seconds', 0) for it in iterations)

# Budget
budget_limit = 0
budget_used = 0
budget_file = os.path.join(loki_dir, 'metrics', 'budget.json')
if os.path.isfile(budget_file):
    try:
        with open(budget_file) as f:
            bd = json.load(f)
        budget_limit = bd.get('budget_limit', 0)
        budget_used = bd.get('budget_used', 0)
    except Exception:
        pass

# Quality gates
gates_passed = 0
gates_total = 0
gates_file = os.path.join(loki_dir, 'state', 'quality-gates.json')
if os.path.isfile(gates_file):
    try:
        with open(gates_file) as f:
            gates = json.load(f)
        if isinstance(gates, dict):
            for k, v in gates.items():
                if isinstance(v, dict):
                    gates_total += 1
                    if v.get('passed') or v.get('status') == 'passed':
                        gates_passed += 1
                elif isinstance(v, bool):
                    gates_total += 1
                    if v:
                        gates_passed += 1
        elif isinstance(gates, list):
            for g in gates:
                gates_total += 1
                if isinstance(g, dict) and (g.get('passed') or g.get('status') == 'passed'):
                    gates_passed += 1
                elif g is True:
                    gates_passed += 1
    except Exception:
        pass

# Gate failures
gate_failures = {}
gf_file = os.path.join(loki_dir, 'quality', 'gate-failure-count.json')
if os.path.isfile(gf_file):
    try:
        with open(gf_file) as f:
            gate_failures = json.load(f)
        if not isinstance(gate_failures, dict):
            gate_failures = {}
    except Exception:
        pass

# Code reviews
reviews_total = 0
reviews_approved = 0
reviews_revision = 0
quality_dir = os.path.join(loki_dir, 'quality')
if os.path.isdir(quality_dir):
    for fname in os.listdir(quality_dir):
        if not fname.endswith('.json') or fname == 'gate-failure-count.json':
            continue
        fpath = os.path.join(quality_dir, fname)
        try:
            with open(fpath) as f:
                rev = json.load(f)
            if isinstance(rev, dict) and ('verdict' in rev or 'approved' in rev or 'reviewers' in rev):
                reviews_total += 1
                verdict = rev.get('verdict', '').lower()
                if rev.get('approved') or verdict in ('approved', 'approve', 'pass'):
                    reviews_approved += 1
                elif verdict in ('revision', 'revise', 'changes_requested', 'reject'):
                    reviews_revision += 1
        except Exception:
            pass

# --- Format duration helper ---
def fmt_duration(secs):
    secs = int(secs)
    if secs < 60:
        return f'{secs}s'
    hours = secs // 3600
    mins = (secs % 3600) // 60
    secs_rem = secs % 60
    if hours > 0:
        return f'{hours}h {mins:02d}m'
    return f'{mins}m {secs_rem:02d}s'

def fmt_number(n):
    return f'{n:,}'

# --- JSON output ---
if show_json:
    output = {
        'session': {
            'iterations': iteration_count,
            'duration_seconds': total_duration,
            'phase': phase
        },
        'tokens': {
            'input': total_input,
            'output': total_output,
            'total': total_tokens,
            'cost_usd': round(total_cost, 2)
        },
        'quality': {
            'gates_passed': gates_passed,
            'gates_total': gates_total,
            'reviews_total': reviews_total,
            'reviews_approved': reviews_approved,
            'reviews_revision': reviews_revision,
            'gate_failures': gate_failures
        },
        'efficiency': {
            'avg_tokens_per_iteration': round(total_tokens / iteration_count) if iteration_count > 0 else 0,
            'avg_cost_per_iteration': round(total_cost / iteration_count, 2) if iteration_count > 0 else 0,
            'avg_duration_per_iteration': round(total_duration / iteration_count, 1) if iteration_count > 0 else 0
        },
        'budget': {
            'used': round(budget_used, 2),
            'limit': budget_limit,
            'percent': round((budget_used / budget_limit) * 100, 1) if budget_limit > 0 else 0
        }
    }
    if show_efficiency:
        output['iterations'] = []
        for i, it in enumerate(iterations, 1):
            output['iterations'].append({
                'number': i,
                'input_tokens': it.get('input_tokens', 0),
                'output_tokens': it.get('output_tokens', 0),
                'cost_usd': round(it.get('cost_usd', 0), 2),
                'duration_seconds': it.get('duration_seconds', 0)
            })
    print(json.dumps(output, indent=2))
    sys.exit(0)

# --- Text output ---
print('Loki Mode Session Statistics')
print('============================')
print()

# Session
print('Session')
print(f'  Iterations completed: {iteration_count}')
print(f'  Duration: {fmt_duration(total_duration)}')
print(f'  Current phase: {phase}')
print()

# Token Usage
print('Token Usage')
if iterations:
    print(f'  Input tokens:  {fmt_number(total_input)}')
    print(f'  Output tokens: {fmt_number(total_output)}')
    print(f'  Total tokens:  {fmt_number(total_tokens)}')
    if cost_measured:
        print(f'  Estimated cost: \${total_cost:.2f}')
    else:
        print('  Estimated cost: unmeasured')
else:
    print('  N/A (no iteration metrics found)')
print()

# Quality Gates
print('Quality Gates')
if gates_total > 0:
    pct = round((gates_passed / gates_total) * 100) if gates_total > 0 else 0
    print(f'  Gates passed: {gates_passed}/{gates_total} ({pct}%)')
else:
    print('  Gates passed: N/A')
if reviews_total > 0:
    parts = []
    if reviews_approved > 0:
        parts.append(f'{reviews_approved} approved')
    if reviews_revision > 0:
        parts.append(f'{reviews_revision} revision requested')
    detail = ', '.join(parts) if parts else 'N/A'
    print(f'  Code reviews: {reviews_total} ({detail})')
if gate_failures:
    failure_parts = [f'{k} ({v})' for k, v in gate_failures.items() if v > 0]
    if failure_parts:
        sep = ', '
        print(f'  Gate failures: {sep.join(failure_parts)}')
print()

# Efficiency
print('Efficiency')
if iteration_count > 0 and iterations:
    avg_tokens = round(total_tokens / iteration_count)
    avg_cost = total_cost / iteration_count
    avg_dur = total_duration / iteration_count
    print(f'  Avg tokens/iteration: {fmt_number(avg_tokens)}')
    print(f'  Avg cost/iteration: \${avg_cost:.2f}')
    print(f'  Avg duration/iteration: {fmt_duration(avg_dur)}')
else:
    print('  N/A (no iteration metrics found)')
print()

# Budget
print('Budget')
if budget_limit > 0:
    pct = round((budget_used / budget_limit) * 100, 1)
    print(f'  Used: \${budget_used:.2f} / \${budget_limit:.2f} ({pct}%)')
elif budget_used > 0:
    print(f'  Used: \${budget_used:.2f} (no limit set)')
else:
    print('  N/A')

# Per-iteration breakdown
if show_efficiency and iterations:
    print()
    print('Per-Iteration Breakdown')
    for i, it in enumerate(iterations, 1):
        inp = fmt_number(it.get('input_tokens', 0))
        out = fmt_number(it.get('output_tokens', 0))
        cost = it.get('cost_usd', 0)
        dur = fmt_duration(it.get('duration_seconds', 0))
        print(f'  #{i:<3} input: {inp:<10} output: {out:<10} cost: \${cost:.2f}  time: {dur}')
" "$loki_dir" "$eff_flag" "$json_flag"
}

# Provider management
cmd_provider() {
    local subcommand="${1:-show}"
    shift || true

    case "$subcommand" in
        show|current)
            cmd_provider_show "$@"
            ;;
        set)
            cmd_provider_set "$@"
            ;;
        list)
            case " $* " in
                *" --json "*) _loki_bun_delegate provider list "$@" || { echo "loki provider list --json needs bun: ${_LOKI_DELEGATE_WHY}" >&2; return 1; } ;;
            esac
            cmd_provider_list
            ;;
        info)
            case " $* " in
                *" --json "*) _loki_bun_delegate provider info "$@" || { echo "loki provider info --json needs bun: ${_LOKI_DELEGATE_WHY}" >&2; return 1; } ;;
            esac
            cmd_provider_info "$@"
            ;;
        matrix)
            _loki_bun_delegate provider matrix "$@" || { echo "loki provider matrix needs bun: ${_LOKI_DELEGATE_WHY}" >&2; return 1; }
            ;;
        models)
            cmd_provider_models
            ;;
        help|--help|-h)
            _cmd_provider_help
            ;;
        *)
            echo "loki provider: unknown subcommand '${subcommand}' (see 'loki provider help')" >&2
            return 2
            ;;
    esac
}

_cmd_provider_help() {
            echo -e "${BOLD}Loki Mode Provider Management${NC}"
            echo ""
            echo "Usage: loki provider <command>"
            echo ""
            echo "Commands:"
            echo "  show     Show current provider (default)"
            echo "  set      Set provider for this project"
            echo "  list     List available providers (--json: name, tier, installed, version)"
            echo "  info     Show provider details (--json: capability contract)"
            echo "  matrix   Show the capability matrix per provider and engine"
            echo "  models   Show resolved model configuration for all providers"
            echo ""
            echo "Precedence (highest wins):"
            echo "  1. loki start --provider NAME         CLI flag (per-invocation)"
            echo "  2. .loki/state/provider               saved value (per-project)"
            echo "  3. LOKI_PROVIDER env var              shell session default"
            echo "  4. claude                             built-in default"
            echo ""
            echo "  Note: 'loki status' reflects the SAVED provider, not the env var."
            echo "  Run 'loki provider set <name>' to make a project-level choice persist."
            echo ""
            echo "Examples:"
            echo "  loki provider show"
            echo "  loki provider set claude"
            echo "  loki provider set codex"
            echo "  loki provider list"
            echo "  loki provider info codex"
            echo "  loki provider models"
}

cmd_provider_show() {
    local positional_arg="${1:-}"

    local saved_provider=""
    if [ -f "$LOKI_DIR/state/provider" ]; then
        saved_provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
    fi

    local current="${positional_arg:-${saved_provider:-${LOKI_PROVIDER:-claude}}}"

    echo -e "${BOLD}Current Provider${NC}"
    echo ""
    echo -e "${CYAN}Provider:${NC} $current"

    # Show capability status
    case "$current" in
        claude)
            echo -e "${GREEN}Status:${NC}   Full features (subagents, parallel, MCP)"
            ;;
        cline)
            echo -e "${GREEN}Status:${NC}   Near-full mode (subagents, MCP, 12+ providers)"
            ;;
        codex|aider)
            echo -e "${YELLOW}Status:${NC}   Degraded mode (sequential only)"
            ;;
        opencode)
            echo -e "${GREEN}Status:${NC}   Model-agnostic mode (75+ providers, MCP)"
            ;;
    esac

    if [ -n "$saved_provider" ]; then
        echo -e "${DIM}(saved in .loki/state/provider)${NC}"
    else
        echo -e "${DIM}(default - not explicitly set)${NC}"
    fi

    echo ""
    echo -e "Switch provider: ${CYAN}loki provider set <name>${NC}"
    echo -e "Available:       ${CYAN}loki provider list${NC}"
}

cmd_provider_set() {
    local new_provider="${1:-}"

    if [ -z "$new_provider" ]; then
        echo -e "${RED}Error: Provider name required${NC}"
        echo "Usage: loki provider set <claude|codex|cline|aider|opencode>"
        exit 2
    fi

    # Validate provider
    case "$new_provider" in
        gemini)
            echo -e "${RED}Error: Provider 'gemini' is deprecated as of v7.5.18 and has been removed.${NC}"
            echo "Active providers: claude, codex, cline, aider, opencode"
            exit 1
            ;;
        claude|codex|cline|aider|opencode)
            ;;
        *)
            echo -e "${RED}Error: Invalid provider '$new_provider'${NC}"
            echo "Valid providers: claude, codex, cline, aider, opencode"
            exit 1
            ;;
    esac

    # Check if CLI is installed
    if ! command -v "$new_provider" &> /dev/null; then
        echo -e "${YELLOW}Warning: '$new_provider' CLI not found in PATH${NC}"
        echo ""
        case "$new_provider" in
            claude)
                echo "Install: npm install -g @anthropic-ai/claude-code"
                ;;
            codex)
                echo "Install: npm install -g @openai/codex"
                ;;
            cline)
                echo "Install: npm install -g cline"
                ;;
            aider)
                echo "Install: pip install aider-chat"
                ;;
            opencode)
                echo "Install: npm install -g opencode-ai"
                ;;
        esac
        echo ""
        echo -e "${DIM}Setting provider anyway...${NC}"
    fi

    # Save provider
    mkdir -p "$LOKI_DIR/state"
    echo "$new_provider" > "$LOKI_DIR/state/provider"

    echo -e "${GREEN}Provider set to: $new_provider${NC}"
    echo ""
    echo "This will be used for all future runs in this project."
    echo "Override temporarily with: loki start --provider <name>"
}

cmd_provider_list() {
    echo -e "${BOLD}Available Providers${NC}"
    echo ""

    local saved_provider=""
    if [ -f "$LOKI_DIR/state/provider" ]; then
        saved_provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
    fi
    local current="${saved_provider:-${LOKI_PROVIDER:-claude}}"

    # Check which CLIs are installed
    local claude_status="${RED}not installed${NC}"
    local codex_status="${RED}not installed${NC}"
    local cline_status="${RED}not installed${NC}"
    local aider_status="${RED}not installed${NC}"
    local opencode_status="${RED}not installed${NC}"

    if command -v claude &> /dev/null; then
        claude_status="${GREEN}installed${NC}"
    fi
    if command -v codex &> /dev/null; then
        codex_status="${GREEN}installed${NC}"
    fi
    if command -v cline &> /dev/null; then
        cline_status="${GREEN}installed${NC}"
    fi
    if command -v aider &> /dev/null; then
        aider_status="${GREEN}installed${NC}"
    fi
    if command -v opencode &> /dev/null; then
        opencode_status="${GREEN}installed${NC}"
    fi

    # Display providers
    local marker=""
    [ "$current" = "claude" ] && marker=" ${CYAN}(current)${NC}"
    echo -e "  claude  - Claude Code (Anthropic)     $claude_status$marker"

    marker=""
    [ "$current" = "codex" ] && marker=" ${CYAN}(current)${NC}"
    echo -e "  codex   - Codex CLI (OpenAI)          $codex_status$marker"

    marker=""
    [ "$current" = "cline" ] && marker=" ${CYAN}(current)${NC}"
    echo -e "  cline   - Cline (multi-provider)      $cline_status$marker"

    marker=""
    [ "$current" = "aider" ] && marker=" ${CYAN}(current)${NC}"
    echo -e "  aider   - Aider (terminal pair prog)  $aider_status$marker"

    # opencode was absent here while auto_detect_provider() has selected it
    # since v8.64.0, so `loki provider list` under-reported a provider the
    # runner will happily choose -- the same drift that blocked opencode-only
    # machines outright until v8.76.0.
    marker=""
    [ "$current" = "opencode" ] && marker=" ${CYAN}(current)${NC}"
    echo -e "  opencode - opencode (multi-provider)  $opencode_status$marker"

    echo ""
    echo -e "Set provider: ${CYAN}loki provider set <name>${NC}"
}

cmd_provider_info() {
    local provider="${1:-${LOKI_PROVIDER:-claude}}"

    echo -e "${BOLD}Provider: $provider${NC}"
    echo ""

    case "$provider" in
        claude)
            echo "Name:        Claude Code"
            echo "Vendor:      Anthropic"
            echo "CLI:         claude"
            echo "Flag:        --dangerously-skip-permissions"
            echo ""
            echo "Features:"
            echo "  - Full autonomous mode"
            echo "  - Task tool for subagents"
            echo "  - Parallel execution"
            echo "  - MCP server support"
            echo ""
            echo "Status:      Full features"
            ;;
        codex)
            echo "Name:        Codex CLI"
            echo "Vendor:      OpenAI"
            echo "CLI:         codex"
            echo "Flag:        --sandbox workspace-write"
            echo ""
            echo "Features:"
            echo "  - Autonomous mode"
            echo "  - Sequential only (no subagents)"
            echo ""
            echo "Status:      Degraded mode"
            ;;
        cline)
            echo "Name:        Cline CLI (Multi-Provider)"
            echo "Vendor:      Cline (supports 12+ providers)"
            echo "CLI:         cline"
            echo "Flag:        -y (YOLO mode)"
            echo ""
            echo "Features:"
            echo "  - Autonomous mode"
            echo "  - Subagent support"
            echo "  - MCP server support"
            echo "  - 12+ model providers"
            echo ""
            echo "Status:      Near-full mode (Tier 2)"
            ;;
        aider)
            echo "Name:        Aider (18+ Providers)"
            echo "Vendor:      Aider (supports 18+ providers)"
            echo "CLI:         aider"
            echo "Flag:        --yes-always"
            echo ""
            echo "Features:"
            echo "  - Autonomous mode"
            echo "  - 18+ model providers"
            echo "  - Architect mode (dual model)"
            echo "  - Built-in lint/test integration"
            echo ""
            echo "Status:      Degraded mode (Tier 3)"
            ;;
        opencode)
            echo "Name:        opencode (75+ Providers)"
            echo "Vendor:      opencode (model-agnostic registry)"
            echo "CLI:         opencode"
            echo "Flag:        run --auto"
            echo ""
            echo "Features:"
            echo "  - Autonomous mode"
            echo "  - 75+ model providers and custom endpoints"
            echo "  - Local model support"
            echo "  - MCP server support"
            echo ""
            echo "Status:      Model-agnostic mode"
            ;;
        *)
            echo -e "${RED}Unknown provider: $provider${NC}"
            exit 1
            ;;
    esac
}

# Show resolved model configuration for all providers with source attribution
cmd_provider_models() {
    local script_dir
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

    echo -e "${BOLD}Model Configuration (resolved):${NC}"
    echo ""

    echo -e "  ${CYAN}Ask for a tier by capability -- small, medium or high -- and each"
    echo -e "  provider supplies its own latest model in that class. medium is the"
    echo -e "  default. Set one with LOKI_SESSION_MODEL=small or --session-model small.${NC}"
    echo ""

    # opencode ships a provider file and a catalog entry, so omitting it here
    # under-reported what this command claims to show ("all providers").
    local providers=("claude" "codex" "cline" "aider" "opencode")
    for provider in "${providers[@]}"; do
        local provider_file="$script_dir/providers/${provider}.sh"
        [ -f "$provider_file" ] || continue

        echo -e "  ${BOLD}Provider: $provider${NC}"

        # Source in subshell to get resolved values
        local planning dev fast
        planning=$(bash -c "source '$provider_file' 2>/dev/null; echo \$PROVIDER_MODEL_PLANNING")
        dev=$(bash -c "source '$provider_file' 2>/dev/null; echo \$PROVIDER_MODEL_DEVELOPMENT")
        fast=$(bash -c "source '$provider_file' 2>/dev/null; echo \$PROVIDER_MODEL_FAST")

        # Determine source for each tier
        local provider_upper
        provider_upper=$(echo "$provider" | tr '[:lower:]' '[:upper:]')

        for tier in planning development fast; do
            local tier_upper
            tier_upper=$(echo "$tier" | tr '[:lower:]' '[:upper:]')
            local source="default"

            # Single-model providers (aider, cline) only chain through their own env var
            # and LOKI_MODEL_DEVELOPMENT -- they ignore per-tier and generic tier vars
            if [ "$provider" = "aider" ]; then
                if [ -n "${LOKI_AIDER_MODEL+x}" ]; then
                    source="LOKI_AIDER_MODEL"
                elif [ -n "${LOKI_MODEL_DEVELOPMENT+x}" ]; then
                    source="LOKI_MODEL_DEVELOPMENT"
                fi
            elif [ "$provider" = "cline" ]; then
                if [ -n "${LOKI_CLINE_MODEL+x}" ]; then
                    source="LOKI_CLINE_MODEL"
                elif [ -n "${LOKI_MODEL_DEVELOPMENT+x}" ]; then
                    source="LOKI_MODEL_DEVELOPMENT"
                fi
            elif [ "$provider" = "codex" ]; then
                # Codex uses single LOKI_CODEX_MODEL or generic tier vars
                if [ -n "${LOKI_CODEX_MODEL+x}" ]; then
                    source="LOKI_CODEX_MODEL"
                else
                    local generic_var="LOKI_MODEL_${tier_upper}"
                    eval "local gval=\${$generic_var+x}"
                    if [ -n "$gval" ]; then
                        source="$generic_var"
                    fi
                fi
            else
                # Multi-tier providers (claude): check provider-specific per-tier, then generic
                local provider_var="LOKI_${provider_upper}_MODEL_${tier_upper}"
                local generic_var="LOKI_MODEL_${tier_upper}"

                eval "local pval=\${$provider_var+x}"
                if [ -n "$pval" ]; then
                    source="$provider_var"
                else
                    eval "local gval=\${$generic_var+x}"
                    if [ -n "$gval" ]; then
                        source="$generic_var"
                    fi
                fi
            fi

            local value
            case "$tier" in
                planning)    value="$planning" ;;
                development) value="$dev" ;;
                fast)        value="$fast" ;;
            esac

            # Label each tier with the GENERIC vocabulary users actually select
            # (small|medium|high), so this command answers "what do I really get
            # if I ask for medium on this provider" without anyone having to know
            # a vendor model name. The canonical tier name stays alongside it
            # because every env var, log line and state file still uses it -- and
            # a user reading LOKI_CLAUDE_MODEL_DEVELOPMENT needs to see the two
            # spellings connected. Mapping mirrors loki_tier_alias() in
            # providers/models.sh.
            local generic
            case "$tier" in
                fast)        generic="small" ;;
                development) generic="medium" ;;
                planning)    generic="high" ;;
                *)           generic="$tier" ;;
            esac

            # An EMPTY resolved model is a real, supported state, not a lookup
            # failure: providers/codex.sh sets CODEX_DEFAULT_MODEL="" on purpose
            # so no --model flag is passed and Codex resolves an
            # account-appropriate default (a hardcoded name broke ChatGPT-account
            # users outright). Say that plainly instead of printing a blank
            # column. Deliberately NOT filled in from the catalog: this table
            # reports what will be DISPATCHED, and the catalog id would be a
            # guess this file is documented as unable to make.
            [ -n "$value" ] || value="(provider default -- no --model sent)"

            printf "    %-8s %-14s %-38s (source: %s)\n" \
                "$generic" "(${tier})" "$value" "$source"
        done

        # Show extra info per provider
        if [ "$provider" = "codex" ]; then
            local effort_p effort_d effort_f
            effort_p=$(bash -c "source '$provider_file' 2>/dev/null; echo \$PROVIDER_EFFORT_PLANNING")
            effort_d=$(bash -c "source '$provider_file' 2>/dev/null; echo \$PROVIDER_EFFORT_DEVELOPMENT")
            effort_f=$(bash -c "source '$provider_file' 2>/dev/null; echo \$PROVIDER_EFFORT_FAST")
            echo "    Effort:      planning=$effort_p, development=$effort_d, fast=$effort_f"
        fi
        echo ""
    done
}

# Standalone dashboard server management (`loki dashboard`).
# State lives at a FIXED, machine-global ~/.loki/dashboard so that
# stop|status|open find the running server from ANY working directory.
# (The relative ${LOKI_DIR}/dashboard path made these commands silently
# fail from a different cwd than `start`, orphaning the server.) The
# separate in-build dashboard started by run.sh during `loki start` stays
# project-local and is unaffected by this constant.
DASHBOARD_PID_DIR="${HOME}/.loki/dashboard"
DASHBOARD_PID_FILE="${DASHBOARD_PID_DIR}/dashboard.pid"
DASHBOARD_DEFAULT_PORT=57374
# Default bind host: 0.0.0.0 inside a container (so a -p port map reaches
# the server; 127.0.0.1 would be unreachable from the host), else loopback
# for host safety. Detect Docker via /.dockerenv or LOKI_SANDBOX_MODE.
if [ -f /.dockerenv ] || [ "${LOKI_SANDBOX_MODE:-}" = "true" ] || [ -n "${LOKI_IN_CONTAINER:-}" ]; then
    DASHBOARD_DEFAULT_HOST="0.0.0.0"
else
    DASHBOARD_DEFAULT_HOST="127.0.0.1"
fi

# EXPOSED BIND REQUIRES AUTH (issue #188).
#
# dashboard/auth.py:788 returns True -- allow -- when neither LOKI_ENTERPRISE_AUTH
# nor OIDC is configured. That makes all 166 Depends(require_scope) endpoints
# open. On loopback that is defensible: the trust boundary is the machine.
# Bound to 0.0.0.0 it is not, and NEITHER the Dockerfile NOR the Helm chart set
# any auth env, so an operator following the shipped chart gets an
# unauthenticated dashboard on every interface without choosing it.
#
# Refuses to start rather than auto-minting a token. An auto-token an operator
# never sees is worse than a refusal: they believe they are protected, and the
# credential is sitting in a container they did not know to guard. A refusal is
# loud, happens at deploy time rather than after exposure, and names the fix.
#
# LOKI_DASHBOARD_ALLOW_INSECURE_BIND=1 is the deliberate override, for a cluster
# whose network policy already bounds ingress. Named so it appears in a diff and
# a reviewer can ask why it is set.
_loki_dashboard_bind_is_safe() {
    local _host="${1:-}"
    case "$_host" in
        127.0.0.1|localhost|::1|"") return 0 ;;
    esac
    [ "${LOKI_DASHBOARD_ALLOW_INSECURE_BIND:-}" = "1" ] && return 0
    [ "${LOKI_ENTERPRISE_AUTH:-}" = "true" ] || [ "${LOKI_ENTERPRISE_AUTH:-}" = "1" ] && return 0
    [ -n "${LOKI_OIDC_ISSUER:-}" ] && [ -n "${LOKI_OIDC_CLIENT_ID:-}" ] && return 0
    return 1
}

_loki_dashboard_args_need_classic() {
    local a
    for a in "$@"; do
        case "$a" in
            --host|--host=*|--tls-cert|--tls-cert=*|--tls-key|--tls-key=*) return 0 ;;
        esac
    done
    return 1
}

cmd_dashboard() {
    local subcommand="${1:-}"

    # CP-LEGACY: the Control Plane is the default UI for `dashboard`,
    # `dashboard start` and `dashboard open`, with or without flags. The legacy
    # Python dashboard stays reachable only by explicit opt-in:
    # LOKI_LEGACY_DASHBOARD=1 or LOKI_CONTROL_DEFAULT=0. Enterprise bind/TLS
    # flags (--host, --tls-cert, --tls-key) are classic-server concepts and
    # keep the classic server.
    case "$subcommand" in
        ""|start|open)
            if [ "${LOKI_LEGACY_DASHBOARD:-0}" != "1" ] && ! _loki_dashboard_args_need_classic "${@:2}"; then
                _loki_control_ui "${@:2}" && return 0
            fi
            ;;
    esac

    # If no subcommand provided, show help (backward compatible with old behavior)
    if [ -z "$subcommand" ]; then
        cmd_dashboard_help
        exit 0
    fi

    shift || true

    case "$subcommand" in
        start)
            cmd_dashboard_start "$@"
            ;;
        stop)
            cmd_dashboard_stop
            ;;
        status)
            cmd_dashboard_status
            ;;
        url)
            cmd_dashboard_url "$@"
            ;;
        open)
            cmd_dashboard_open
            ;;
        --help|-h|help)
            cmd_dashboard_help
            ;;
        *)
            echo -e "${RED}Unknown dashboard command: $subcommand${NC}"
            echo "Run 'loki dashboard help' for usage."
            exit 1
            ;;
    esac
}

cmd_dashboard_help() {
    echo -e "${BOLD}Loki Mode Dashboard Server${NC}"
    echo ""
    echo "Usage: loki dashboard <command> [options]"
    echo ""
    echo "'loki dashboard', 'loki dashboard start' and 'loki dashboard open' open the Control Plane,"
    echo "the browser UI for runs, cost, receipts and events (default port 47821; override with"
    echo "LOKI_CONTROL_PORT). Needs bun; see 'loki control --help'."
    echo "The commands below manage the classic dashboard server, which serves the API only."
    echo "Setting LOKI_LEGACY_DASHBOARD=1, or passing --host, --tls-cert or --tls-key, selects it."
    echo ""
    echo "Commands:"
    echo "  start    Open the Control Plane (or start the classic API server with the flags below)"
    echo "  stop     Stop the dashboard server"
    echo "  status   Show server status and URL"
    echo "  url      Get the dashboard URL"
    echo "  open     Open dashboard in browser"
    echo "  help     Show this help"
    echo ""
    echo "Options for 'start':"
    echo "  --port PORT        Port for the classic API server (default: see LOKI_DASHBOARD_PORT)"
    echo "  --host HOST        Host to bind to (default: $DASHBOARD_DEFAULT_HOST)"
    echo "  --tls-cert PATH    Path to PEM certificate (enables HTTPS)"
    echo "  --tls-key PATH     Path to PEM private key (enables HTTPS)"
    echo ""
    echo "Options for 'url':"
    echo "  --format FMT   Output format: text (default) or json"
    echo ""
    echo "Environment:"
    echo "  LOKI_DASHBOARD_PORT  Port for the classic API server"
    echo "  LOKI_DASHBOARD_HOST  Default host (overrides $DASHBOARD_DEFAULT_HOST)"
    echo "  LOKI_TLS_CERT        Path to PEM certificate (enables HTTPS)"
    echo "  LOKI_TLS_KEY         Path to PEM private key (enables HTTPS)"
    echo ""
    echo "Examples:"
    echo "  loki dashboard start                  # Start on default port"
    echo "  loki dashboard start --port 8080      # Start on port 8080"
    echo "  loki dashboard start --tls-cert cert.pem --tls-key key.pem"
    echo "  loki dashboard status                 # Check if running"
    echo "  loki dashboard url --format json      # Get URL as JSON"
    echo "  loki dashboard open                   # Open the Control Plane"
    echo "  loki dashboard stop                   # Stop server"
}

cmd_dashboard_start() {
    local port="${LOKI_DASHBOARD_PORT:-$DASHBOARD_DEFAULT_PORT}"
    local host="${LOKI_DASHBOARD_HOST:-$DASHBOARD_DEFAULT_HOST}"
    local tls_cert="${LOKI_TLS_CERT:-}"
    local tls_key="${LOKI_TLS_KEY:-}"

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --port)
                if [[ -n "${2:-}" ]]; then
                    port="$2"
                    shift 2
                else
                    echo -e "${RED}--port requires a value${NC}"
                    exit 1
                fi
                ;;
            --port=*)
                port="${1#*=}"
                shift
                ;;
            --host)
                if [[ -n "${2:-}" ]]; then
                    host="$2"
                    shift 2
                else
                    echo -e "${RED}--host requires a value${NC}"
                    exit 1
                fi
                ;;
            --host=*)
                host="${1#*=}"
                shift
                ;;
            --tls-cert)
                if [[ -n "${2:-}" ]]; then
                    tls_cert="$2"
                    shift 2
                else
                    echo -e "${RED}--tls-cert requires a path${NC}"
                    exit 1
                fi
                ;;
            --tls-cert=*)
                tls_cert="${1#*=}"
                shift
                ;;
            --tls-key)
                if [[ -n "${2:-}" ]]; then
                    tls_key="$2"
                    shift 2
                else
                    echo -e "${RED}--tls-key requires a path${NC}"
                    exit 1
                fi
                ;;
            --tls-key=*)
                tls_key="${1#*=}"
                shift
                ;;
            --help|-h)
                cmd_dashboard_help
                exit 0
                ;;
            *)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki dashboard start --help' for usage."
                exit 1
                ;;
        esac
    done

    # Refuse an exposed bind with auth disabled (issue #188). Checked AFTER
    # argument parsing so an explicit --host is covered, not just the default.
    if ! _loki_dashboard_bind_is_safe "$host"; then
        echo -e "${RED}Refusing to start: binding $host with authentication disabled.${NC}" >&2
        echo "" >&2
        echo "  Every dashboard endpoint would be reachable on all interfaces with no" >&2
        echo "  credential. dashboard/auth.py allows all requests when neither" >&2
        echo "  LOKI_ENTERPRISE_AUTH nor OIDC is configured." >&2
        echo "" >&2
        echo "  Pick one:" >&2
        echo "    LOKI_ENTERPRISE_AUTH=true          enable token auth (loki dashboard token create)" >&2
        echo "    LOKI_OIDC_ISSUER + _CLIENT_ID      enable OIDC" >&2
        echo "    --host 127.0.0.1                   bind loopback only" >&2
        echo "    LOKI_DASHBOARD_ALLOW_INSECURE_BIND=1   accept the exposure deliberately" >&2
        exit 2
    fi

    # Validate port is numeric
    if [[ ! "$port" =~ ^[0-9]+$ ]]; then
        echo -e "${RED}Invalid port: $port${NC}"
        echo "Port must be a number."
        exit 1
    fi

    # Check if Python is available
    if ! command -v python &> /dev/null && ! command -v python3 &> /dev/null; then
        echo -e "${RED}Error: Python not found${NC}"
        echo "Python is required for the dashboard server."
        echo "Install with: brew install python"
        exit 1
    fi

    # Set up dashboard venv and python command
    if ! ensure_dashboard_venv; then
        echo -e "${RED}Cannot start dashboard without Python dependencies${NC}"
        return 1
    fi
    local python_cmd="$DASHBOARD_PYTHON"

    # Check if already running
    if [ -f "$DASHBOARD_PID_FILE" ]; then
        local existing_pid
        existing_pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)
        if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null \
            && _loki_dashboard_port_is_ours "$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)"; then
            echo -e "${YELLOW}Dashboard server already running (PID: $existing_pid)${NC}"
            echo ""
            cmd_dashboard_status
            exit 0
        fi
        # Stale PID file, or a live server that is not this package's dashboard
        # (never killed here, just not reused): remove the registry entry.
        rm -f "$DASHBOARD_PID_FILE"
    fi

    # Check if port is already in use
    if command -v lsof &> /dev/null; then
        # LISTEN only: leftover client sockets (browser CLOSE_WAIT) do not hold the port
        if lsof -nP -iTCP:"$port" -sTCP:LISTEN &> /dev/null; then
            echo -e "${RED}Error: Port $port is already in use${NC}"
            echo ""
            echo "Check what's using the port:"
            echo "  lsof -nP -iTCP:$port -sTCP:LISTEN"
            echo ""
            echo "Or use a different port:"
            echo "  loki dashboard start --port 8080"
            exit 1
        fi
    fi

    # Ensure PID directory exists
    mkdir -p "$DASHBOARD_PID_DIR"

    # Create log directory
    local log_dir="${DASHBOARD_PID_DIR}/logs"
    mkdir -p "$log_dir"
    local log_file="${log_dir}/dashboard.log"

    # Determine URL scheme based on TLS
    local url_scheme="http"
    local tls_info=""
    if [ -n "$tls_cert" ] && [ -n "$tls_key" ]; then
        url_scheme="https"
        tls_info=" (TLS enabled)"
    fi

    echo -e "${GREEN}Starting dashboard server...${NC}"
    echo -e "${CYAN}Host:${NC} $host"
    echo -e "${CYAN}Port:${NC} $port"
    if [ -n "$tls_info" ]; then
        echo -e "${CYAN}TLS:${NC}  enabled"
    fi
    echo ""

    # Start the dashboard server in background
    # LOKI_SKILL_DIR tells server.py where to find static files
    LOKI_DIR="$LOKI_DIR" LOKI_SKILL_DIR="$SKILL_DIR" PYTHONPATH="$SKILL_DIR" \
        LOKI_DASHBOARD_HOST="$host" LOKI_DASHBOARD_PORT="$port" \
        LOKI_TLS_CERT="$tls_cert" LOKI_TLS_KEY="$tls_key" \
        nohup "$python_cmd" -m dashboard.server > "$log_file" 2>&1 &
    local new_pid=$!

    # Save PID
    echo "$new_pid" > "$DASHBOARD_PID_FILE"

    # Also save port and host for status command
    echo "$port" > "${DASHBOARD_PID_DIR}/port"
    echo "$host" > "${DASHBOARD_PID_DIR}/host"
    echo "$url_scheme" > "${DASHBOARD_PID_DIR}/scheme"

    # Wait for the dashboard to become ready (up to 10 seconds). Probe the
    # UNAUTHENTICATED /health endpoint over the ACTUAL scheme: /api/status
    # 401s under LOKI_ENTERPRISE_AUTH, and the scheme is https when TLS is on,
    # so the old hardcoded http://.../api/status probe always failed against a
    # healthy TLS or auth-enabled server. Use a loopback-reachable host when
    # bound to 0.0.0.0, and -k to tolerate self-signed certs.
    local health_host="$host"; [ "$health_host" = "0.0.0.0" ] && health_host="127.0.0.1"
    local health_curl_opts="-sf"; [ "$url_scheme" = "https" ] && health_curl_opts="-sfk"
    local health_retries=20
    local health_interval=0.5
    local health_ok=false
    while [[ $health_retries -gt 0 ]]; do
        if curl $health_curl_opts "${url_scheme}://${health_host}:${port}/health" >/dev/null 2>&1; then
            health_ok=true
            break
        fi
        # Also check if the process died
        if ! kill -0 "$new_pid" 2>/dev/null; then
            break
        fi
        sleep "$health_interval"
        health_retries=$((health_retries - 1))
    done
    if ! $health_ok && kill -0 "$new_pid" 2>/dev/null; then
        echo -e "${YELLOW}Warning: Dashboard started but health check did not respond within timeout${NC}"
    fi

    if kill -0 "$new_pid" 2>/dev/null; then
        local url="${url_scheme}://${host}:${port}"
        echo -e "${GREEN}Dashboard (live run monitor) started${NC}"
        echo ""
        echo "  PID:  $new_pid"
        echo "  URL:  $url"
        echo "  Logs: $log_file"
        echo ""
        # v7.28.x UX #5: point a newcomer who wants to submit a spec at the right
        # surface. v7.44.0: the standalone Purple Lab (loki web) is deprecated and
        # consolidated here, so steer to the embedded Lab tab instead of spawning
        # a second port.
        echo -e "${DIM}Want to submit a spec in the browser? Open the 'Lab' tab in this dashboard.${NC}"
        echo ""
        echo -e "Control Plane UI: ${CYAN}loki ui${NC}"
        echo -e "Check status:    ${CYAN}loki dashboard status${NC}"
        echo -e "Stop server:     ${CYAN}loki dashboard stop${NC}"
    else
        rm -f "$DASHBOARD_PID_FILE"
        echo -e "${RED}Failed to start dashboard server${NC}"
        echo ""
        echo "Check the log file for errors:"
        echo "  cat $log_file"
        exit 1
    fi
}

cmd_dashboard_stop() {
    if [ ! -f "$DASHBOARD_PID_FILE" ]; then
        echo -e "${YELLOW}Dashboard server is not running${NC}"
        echo "(No PID file found at $DASHBOARD_PID_FILE)"
        exit 0
    fi

    local pid
    pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)

    if [ -z "$pid" ]; then
        rm -f "$DASHBOARD_PID_FILE"
        echo -e "${YELLOW}Dashboard server is not running (empty PID file)${NC}"
        exit 0
    fi

    if kill -0 "$pid" 2>/dev/null; then
        echo -e "${GREEN}Stopping dashboard server (PID: $pid)...${NC}"
        kill "$pid" 2>/dev/null

        # Wait for process to terminate
        local wait_count=0
        while kill -0 "$pid" 2>/dev/null && [ $wait_count -lt 10 ]; do
            sleep 0.5
            wait_count=$((wait_count + 1))
        done

        # Force kill if still running
        if kill -0 "$pid" 2>/dev/null; then
            echo -e "${YELLOW}Process did not terminate gracefully, force killing...${NC}"
            kill -9 "$pid" 2>/dev/null
        fi

        rm -f "$DASHBOARD_PID_FILE"
        echo -e "${GREEN}Dashboard server stopped${NC}"
    else
        rm -f "$DASHBOARD_PID_FILE"
        echo -e "${YELLOW}Dashboard server was not running (stale PID file removed)${NC}"
    fi
}

cmd_dashboard_status() {
    echo -e "${BOLD}Dashboard Server Status${NC}"
    echo ""

    if [ ! -f "$DASHBOARD_PID_FILE" ]; then
        echo -e "${YELLOW}Status: Not running${NC}"
        echo ""
        echo -e "Start with: ${CYAN}loki dashboard start${NC}"
        return 0
    fi

    local pid
    pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)

    if [ -z "$pid" ]; then
        echo -e "${YELLOW}Status: Not running (empty PID file)${NC}"
        rm -f "$DASHBOARD_PID_FILE"
        return 0
    fi

    if kill -0 "$pid" 2>/dev/null; then
        # Read saved host, port, and scheme
        local host="${DASHBOARD_DEFAULT_HOST}"
        local port="${DASHBOARD_DEFAULT_PORT}"
        local scheme="http"

        if [ -f "${DASHBOARD_PID_DIR}/host" ]; then
            host=$(cat "${DASHBOARD_PID_DIR}/host" 2>/dev/null)
        fi
        if [ -f "${DASHBOARD_PID_DIR}/port" ]; then
            port=$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)
        fi
        if [ -f "${DASHBOARD_PID_DIR}/scheme" ]; then
            scheme=$(cat "${DASHBOARD_PID_DIR}/scheme" 2>/dev/null)
        fi

        local url="${scheme}://${host}:${port}"

        echo -e "${GREEN}Status: Running${NC}"
        echo ""
        echo "  PID:  $pid"
        echo "  Host: $host"
        echo "  Port: $port"
        if [ "$scheme" = "https" ]; then
            echo "  TLS:  enabled"
        fi
        echo "  URL:  $url"
        echo ""

        # Check if server is responding
        local curl_flags="-s --connect-timeout 2"
        if [ "$scheme" = "https" ]; then
            curl_flags="$curl_flags -k"
        fi
        if command -v curl &> /dev/null; then
            if curl $curl_flags "$url" &> /dev/null; then
                echo -e "${GREEN}Server is responding${NC}"
            else
                echo -e "${YELLOW}Server process running but not responding on $url${NC}"
                echo "The server may still be starting up."
            fi
        fi

        echo ""
        echo -e "Control Plane UI: ${CYAN}loki ui${NC}"
        echo -e "Stop server:     ${CYAN}loki dashboard stop${NC}"
    else
        echo -e "${YELLOW}Status: Not running (stale PID file)${NC}"
        rm -f "$DASHBOARD_PID_FILE"
        echo ""
        echo -e "Start with: ${CYAN}loki dashboard start${NC}"
    fi
}

cmd_dashboard_url() {
    local format="text"

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --format)
                if [[ -n "${2:-}" ]]; then
                    format="$2"
                    shift 2
                else
                    echo -e "${RED}--format requires a value (text or json)${NC}"
                    exit 1
                fi
                ;;
            --format=*)
                format="${1#*=}"
                shift
                ;;
            --help|-h)
                echo "Usage: loki dashboard url [--format text|json]"
                exit 0
                ;;
            *)
                echo -e "${RED}Unknown option: $1${NC}"
                exit 1
                ;;
        esac
    done

    # Validate format
    if [[ "$format" != "text" && "$format" != "json" ]]; then
        echo -e "${RED}Invalid format: $format${NC}"
        echo "Valid formats: text, json"
        exit 1
    fi

    # Check if running
    if [ ! -f "$DASHBOARD_PID_FILE" ]; then
        if [[ "$format" == "json" ]]; then
            echo '{"running":false,"error":"Dashboard server is not running"}'
        else
            echo -e "${YELLOW}Dashboard server is not running${NC}"
            echo "Start with: loki dashboard start"
        fi
        exit 1
    fi

    local pid
    pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)

    if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
        rm -f "$DASHBOARD_PID_FILE"
        if [[ "$format" == "json" ]]; then
            echo '{"running":false,"error":"Dashboard server is not running"}'
        else
            echo -e "${YELLOW}Dashboard server is not running${NC}"
            echo "Start with: loki dashboard start"
        fi
        exit 1
    fi

    # Read saved host, port, and scheme
    local host="${DASHBOARD_DEFAULT_HOST}"
    local port="${DASHBOARD_DEFAULT_PORT}"
    local scheme="http"

    if [ -f "${DASHBOARD_PID_DIR}/host" ]; then
        host=$(cat "${DASHBOARD_PID_DIR}/host" 2>/dev/null)
    fi
    if [ -f "${DASHBOARD_PID_DIR}/port" ]; then
        port=$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)
    fi
    if [ -f "${DASHBOARD_PID_DIR}/scheme" ]; then
        scheme=$(cat "${DASHBOARD_PID_DIR}/scheme" 2>/dev/null)
    fi

    local url="${scheme}://${host}:${port}"

    if [[ "$format" == "json" ]]; then
        echo "{\"running\":true,\"url\":\"$url\",\"scheme\":\"$scheme\",\"host\":\"$host\",\"port\":$port,\"pid\":$pid}"
    else
        echo "$url"
    fi
}

cmd_dashboard_open() {
    # Check if server is running
    if [ ! -f "$DASHBOARD_PID_FILE" ]; then
        echo -e "${YELLOW}Dashboard server is not running${NC}"
        echo ""
        echo -e "Start with: ${CYAN}loki dashboard start${NC}"
        exit 1
    fi

    local pid
    pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)

    if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
        rm -f "$DASHBOARD_PID_FILE"
        echo -e "${YELLOW}Dashboard server is not running${NC}"
        echo ""
        echo -e "Start with: ${CYAN}loki dashboard start${NC}"
        exit 1
    fi

    # Read saved host, port, and scheme
    local host="${DASHBOARD_DEFAULT_HOST}"
    local port="${DASHBOARD_DEFAULT_PORT}"
    local scheme="http"

    if [ -f "${DASHBOARD_PID_DIR}/host" ]; then
        host=$(cat "${DASHBOARD_PID_DIR}/host" 2>/dev/null)
    fi
    if [ -f "${DASHBOARD_PID_DIR}/port" ]; then
        port=$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)
    fi
    if [ -f "${DASHBOARD_PID_DIR}/scheme" ]; then
        scheme=$(cat "${DASHBOARD_PID_DIR}/scheme" 2>/dev/null)
    fi

    local url="${scheme}://${host}:${port}"

    # The legacy browser UI was removed; this server is API-only. Never open a
    # browser here: point at the Control Plane instead.
    echo -e "${YELLOW}The dashboard server at $url serves the API only; the browser UI is now the Control Plane.${NC}"
    echo -e "Open the Control Plane with: ${CYAN}loki ui${NC}"
}

# Welcome opener (the "magic opener"). Shows a branded welcome page in the
# browser, or a terminal welcome when no browser is available (headless,
# Docker, CI). Analytics are OPT-IN and OFF by default: unless the user has
# opted in via the unified gate (loki_collection_enabled), the page is loaded
# with ?telemetry=off so its form is inert and no analytics are ever sent.
WELCOME_MARKER="${HOME}/.loki/.welcomed"

# _loki_welcome_analytics_on: true only when the unified opt-in gate is on.
# Falls back to "off" (no analytics) if the gate function is unavailable, which
# keeps a default install from ever phoning home.
_loki_welcome_analytics_on() {
    if type loki_collection_enabled &>/dev/null; then
        loki_collection_enabled && return 0
        return 1
    fi
    return 1
}

cmd_welcome_terminal() {
    local ver="${1:-}"
    echo ""
    echo -e "  ${BOLD}Loki Mode${NC} ${DIM}by Autonomi${NC}${ver:+  ${DIM}v${ver}${NC}}"
    echo ""
    echo -e "  ${BOLD}Describe it. Walk away. Get working, tested software.${NC}"
    echo ""
    echo -e "  Loki takes a spec (PRD, GitHub issue, or one-line brief) to a deployed"
    echo -e "  product via the RARV-C closure loop, until the work is actually done."
    echo ""
    echo -e "  ${CYAN}Closure loop + council${NC}   Every change is verified by a unanimous council."
    echo -e "  ${CYAN}Cross-project memory${NC}     Lessons compound, so agents stop repeating mistakes."
    echo -e "  ${CYAN}Provider-agnostic${NC}        Claude, Codex, Cline, or Aider. Your keys, your infra."
    echo ""
    echo -e "  Try it now:   ${BOLD}loki tour${NC} (a real sample result, no provider, no key, no spend)"
    echo -e "  Quick start:  ${BOLD}loki quickstart${NC} (from your idea) or ${BOLD}loki start ./prd.md${NC} (from a spec)"
    echo -e "  Docs:         ${BOLD}https://www.autonomi.dev/docs${NC}"
    echo ""
    # One-time honest disclosure (this welcome screen shows once, via the
    # WELCOME_MARKER). On-by-default anonymous diagnostics must be disclosed
    # somewhere so collection is never covert; this single line is that
    # disclosure. It is NOT repeated on later runs. Enterprise/CI/air-gapped
    # installs auto-disable collection, so the line only appears when it is
    # actually on. Opt out anytime: loki telemetry off.
    if _loki_welcome_analytics_on; then
        echo -e "  ${DIM}Anonymous diagnostics on (os/arch/version/error type only -- never${NC}"
        echo -e "  ${DIM}code, prompts, or paths). Off: loki telemetry off  -  docs/PRIVACY.md${NC}"
        echo ""
    elif [ -n "${LOKI_TELEMETRY_DISABLED:-}" ] || [ -n "${DO_NOT_TRACK:-}" ]; then
        # Off-by-default means silence otherwise (nothing to disclose), but an
        # EXPLICIT opt-out via env var deserves a confirmation that it took
        # effect, distinct from the default off state nobody asked about.
        echo -e "  ${DIM}Analytics are off (LOKI_TELEMETRY_DISABLED/DO_NOT_TRACK).${NC}"
        echo ""
    fi
}

cmd_welcome() {
    local ver=""
    [ -f "${SKILL_DIR}/VERSION" ] && ver="$(cat "${SKILL_DIR}/VERSION" 2>/dev/null | tr -d '[:space:]')"

    local welcome_file="${SKILL_DIR}/assets/welcome/welcome.html"

    # Build the file URL with params: version, opt-in state, and the existing
    # anonymous telemetry distinct-id (so browser + install share one id).
    # Analytics are OFF by default: pass telemetry=off (inert form) UNLESS the
    # user has opted in via the unified gate.
    local q="v=${ver}"
    if _loki_welcome_analytics_on; then
        local idfile="${HOME}/.loki-telemetry-id"
        [ -f "$idfile" ] && q="${q}&did=$(cat "$idfile" 2>/dev/null | tr -d '[:space:]')"
    else
        q="${q}&telemetry=off"
    fi

    # Headless / Docker / CI / tests / no-browser: print the terminal welcome.
    if [ -f "$welcome_file" ] && loki_open_url "file://${welcome_file}?${q}" 2>/dev/null; then
        echo -e "${GREEN}Opened the Loki Mode welcome page in your browser.${NC}"
        return 0
    fi
    cmd_welcome_terminal "$ver"
    [ -f "$welcome_file" ] && echo -e "  ${DIM}Full welcome page: ${welcome_file}${NC}" && echo ""
    return 0
}

# First-run hook: open the welcome once, then never again. Browser only when
# interactive + not CI + not opted out; otherwise nothing (no noisy auto-print
# during a real run). Marker lives at ~/.loki/.welcomed.
cmd_welcome_maybe_firstrun() {
    [ -f "$WELCOME_MARKER" ] && return 0
    mkdir -p "$(dirname "$WELCOME_MARKER")" 2>/dev/null || true
    : > "$WELCOME_MARKER" 2>/dev/null || true
    # Only auto-open on an interactive, non-CI terminal. cmd_welcome itself
    # re-checks for a browser and falls back to the terminal welcome, so we
    # gate purely on interactivity + CI here (grouped to avoid &&/|| precedence
    # ambiguity). Backgrounded so it never delays the start.
    if declare -F loki_browser_allowed >/dev/null 2>&1 && loki_browser_allowed; then
        (cmd_welcome >/dev/null 2>&1 &) || true
    fi
}

# Web app management
# Purple Lab -- standalone product web UI for Loki Mode.
# Runs on port 57375 (separate from dashboard at 57374).
# Backend: web-app/server.py (FastAPI), Frontend: web-app/dist/

PURPLE_LAB_DEFAULT_PORT=57375
PURPLE_LAB_DEFAULT_HOST="127.0.0.1"
# Use HOME-based path so stop works from any CWD
PURPLE_LAB_STATE_DIR="${HOME}/.loki/purple-lab"
PURPLE_LAB_PID_FILE="${PURPLE_LAB_STATE_DIR}/purple-lab.pid"

# Is this pid positively identified as THIS installation's Purple Lab server
# (a server started before the web deprecation launched exactly `$DASHBOARD_PYTHON $SKILL_DIR/web-app/server.py`
# at :8078), rather than a recycled pid, a decoy that merely mentions the path
# in an argument, or another installation's server.py at a different SKILL_DIR?
#
# A substring match on the path is NOT identity: `vim server.py`, `tail -f
# server.py`, or `grep server.py x` in the exact web-app dir would all pass a
# naive `case "$cmd" in *server.py*)`. This instead tokenizes the full command
# line and requires: (1) argv[0] is an (optionally path-qualified) python
# interpreter -- covers "python3", "/usr/bin/python3.12", and the macOS
# Python.app framework binary; (2) argv[1] is EXACTLY this installation's
# absolute web-app/server.py path, not a substring of it; (3) the process is
# owned by this user (SKILL_DIR is frequently a shared global install path, so
# uid alone rules out a same-path process started by someone else).
# Fails closed: an absent/unreadable `ps` result, or anything this cannot
# positively parse, returns "not a match" -- never signal on ambiguity.
_loki_pid_looks_like_purplelab() {
    local _p="$1" _cmd _want _my_uid _p_uid
    case "$_p" in ''|*[!0-9]*) return 1 ;; esac
    [ "$_p" -gt 1 ] 2>/dev/null || return 1
    _want="${SKILL_DIR}/web-app/server.py"
    [ -f "$_want" ] || return 1
    _cmd="$(ps -o command= -p "$_p" 2>/dev/null)"
    [ -n "$_cmd" ] || return 1
    _my_uid="$(id -u 2>/dev/null)" || return 1
    _p_uid="$(ps -o uid= -p "$_p" 2>/dev/null | tr -d ' ')"
    [ -n "$_p_uid" ] && [ "$_p_uid" = "$_my_uid" ] || return 1
    # Tokenize without glob expansion or command substitution (bash 3.2 safe):
    # `set -f` in this function's own subshell-free scope would leak, so do it
    # in a nested subshell and echo the verdict back.
    (
        set -f
        # shellcheck disable=SC2086
        set -- $_cmd
        argv0="${1:-}"
        argv1="${2:-}"
        # Match the BASENAME only. The prior "*/[Pp]ython*|[Pp]ython*" glob
        # matched the WHOLE string against "any chars, then /, then
        # python-substring, then any chars" -- not "the final path segment
        # starts with python" -- so /opt/python-tools/bin/tail passed. Not
        # currently exploitable on its own (argv1 exact-match + uid still
        # gate it), but tightened per council review: a real interpreter's
        # basename must itself start with [Pp]ython, e.g. "python3",
        # "python3.12", or a full path ending in one of those.
        case "${argv0##*/}" in
            [Pp]ython*) ;;
            *) exit 1 ;;
        esac
        [ "$argv1" = "$_want" ] || exit 1
        exit 0
    )
}

cmd_web() {
    local subcommand="${1:-start}"

    # Deprecation (v7.44.0): Purple Lab (loki web, port 57375) is deprecated and
    # consolidated into the dashboard. The hosted/commercial role moved to the
    # separate Autonomi Cloud repo. This is a VALUE-PRESERVING deprecation: the
    # command STILL WORKS (it launches after the banner) so no one is stranded.
    # The banner + telemetry mirror the run->start deprecation contract: print
    # to stderr, suppress under machine-output flags (--json/-q/--quiet/json...),
    # and only emit telemetry when .loki already exists (events/emit.sh creates
    # "$LOKI_DIR/events/pending" as a side effect that would flip downstream
    # guards in a clean directory). The --help/-h/help path shows the deprecation
    # in its own help text instead, so we skip the banner there.
    case "$subcommand" in
        --help|-h|help) ;;
        *)
            if ! _deprecated_alias_should_suppress "$@"; then
                echo "loki web (Purple Lab) is deprecated as of v7.44.0. For local build + monitoring, use the dashboard (auto-launches at 'loki start', http://localhost:${DASHBOARD_DEFAULT_PORT}; or 'loki dashboard'). For the hosted platform, see Autonomi Cloud." >&2
                if [ -d "${LOKI_DIR:-.loki}" ]; then
                    emit_event cli_command_deprecated cli web \
                        "from=web" \
                        "to=dashboard" \
                        "version=7.44.0" \
                        "argv=${*:-}"
                fi
            fi
            ;;
    esac

    case "$subcommand" in
        start)
            shift || true
            cmd_web_redirect_to_dashboard "$@"
            ;;
        stop)
            # `loki web start` redirects to the dashboard (above), so stopping
            # Purple Lab here left the user unable to stop what they had just
            # started: start hit 57374, stop hit 57375. Stop BOTH -- the
            # dashboard because that is what start launched, and Purple Lab
            # because a long-lived pre-deprecation process may still be up.
            cmd_dashboard_stop
            cmd_web_stop
            ;;
        status)
            # Same reasoning as stop: report on what start actually launched.
            cmd_dashboard_status
            cmd_web_status
            ;;
        logs)
            shift || true
            local log_lines="${1:-100}"
            local log_file="${PURPLE_LAB_STATE_DIR}/logs/purple-lab.log"
            if [ ! -f "$log_file" ]; then
                log_file="${LOKI_DIR}/purple-lab/logs/purple-lab.log"
            fi
            if [ ! -f "$log_file" ]; then
                echo -e "${YELLOW}No Purple Lab log file found${NC}"
                return 0
            fi
            echo -e "${BOLD}Purple Lab Logs (last $log_lines lines)${NC}"
            echo -e "${DIM}Use 'loki web logs <N>' to show more/fewer lines${NC}"
            echo ""
            tail -n "$log_lines" "$log_file"
            ;;
        --help|-h|help)
            cmd_web_help
            ;;
        *)
            # Treat unknown args as options to start (e.g., loki web --no-open)
            shift
            cmd_web_redirect_to_dashboard "$subcommand" "$@"
            ;;
    esac
}

# v7.x (F55): `loki web` (the command a user intuitively types) used to launch
# the deprecated Purple Lab, whose '/' redirects to '/lab/' and whose port has
# none of the real dashboard APIs (/api/status, /trust, etc. all 404). A user
# typing the obvious command landed on the wrong, broken-looking surface. The
# value-preserving fix: the START path now launches the real dashboard instead.
# 'loki web stop|status|logs' still operate on Purple Lab so anyone who started
# it the old way (or via scripts) is never stranded. The deprecation banner has
# already printed in cmd_web() before we get here.
cmd_web_redirect_to_dashboard() {
    # The dashboard start path does not accept Purple-Lab-only flags
    # (--no-open, --prd). Filter them so `loki web --no-open` (used in CI) and
    # `loki web --prd X` keep working instead of erroring on an unknown option.
    # --port is shared and passes straight through.
    local orig_args=("$@")
    local dash_args=()
    local _web_dropped_prd=""
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --no-open)
                # Dashboard has no browser-open behavior to suppress; drop it.
                shift
                ;;
            --prd)
                # Purple-Lab-only prefill; the dashboard cannot accept it.
                # Dropping it is correct, doing so SILENTLY is not: the user
                # named a spec file and nothing would tell them it was ignored,
                # so they would sit in front of an empty dashboard believing
                # their PRD had been loaded. Say so, and name the command that
                # actually takes a PRD.
                _web_dropped_prd="${2:-}"
                shift 2 2>/dev/null || shift
                ;;
            --prd=*)
                _web_dropped_prd="${1#--prd=}"
                shift
                ;;
            *)
                dash_args+=("$1")
                shift
                ;;
        esac
    done

    # Safe empty-array expansion (bash 3.2 + set -u): ${arr[@]+"${arr[@]}"}
    if ! _deprecated_alias_should_suppress ${orig_args[@]+"${orig_args[@]}"}; then
        echo "Launching the dashboard (http://localhost:${DASHBOARD_DEFAULT_PORT}) instead of the deprecated Purple Lab." >&2
    fi
    if [ -n "$_web_dropped_prd" ]; then
        echo "Note: --prd '${_web_dropped_prd}' was NOT loaded. The dashboard does not take a PRD;" >&2
        echo "      it watches runs that are already going. To build from that spec:" >&2
        echo "        loki start '${_web_dropped_prd}'" >&2
    fi
    # Flags such as --port must reach the classic validator, so only the
    # bare form (optionally --no-open) is routed to the Control Plane.
    if [ "${LOKI_LEGACY_DASHBOARD:-0}" != "1" ] && [ ${#dash_args[@]} -eq 0 ] && _loki_control_ui; then
        return 0
    fi
    cmd_dashboard_start ${dash_args[@]+"${dash_args[@]}"}
}

cmd_web_help() {
    echo -e "${BOLD}loki web${NC} - Deprecated alias for the Control Plane"
    echo ""
    echo "Usage: loki web [command] [options]"
    echo ""
    echo "The standalone web UI was folded into the Control Plane. 'loki web' with no"
    echo "subcommand launches the Control Plane (default port 47821, override with"
    echo "LOKI_CONTROL_PORT). The canonical command is 'loki dashboard'."
    echo ""
    echo "Commands:"
    echo "  start    Launch the Control Plane (default)"
    echo "  stop     Stop a standalone web server started the old way"
    echo "  status   Show standalone web server status"
    echo "  logs     Show standalone web server logs"
    echo "  help     Show this help"
    echo ""
    echo "Options (for start):"
    echo "  --port PORT    Use custom port"
    echo "  --no-open      Accepted for back-compat; ignored (no-op)"
    echo ""
    echo "Examples:"
    echo "  loki web                  Launch the Control Plane"
    echo "  loki web --no-open        Same (--no-open is a no-op)"
    echo "  loki web stop             Stop a standalone web server started the old way"
}

cmd_web_stop() {
    local stopped=false
    local port
    port=$(cat "${PURPLE_LAB_STATE_DIR}/port" 2>/dev/null || cat "${LOKI_DIR}/purple-lab/port" 2>/dev/null || echo "$PURPLE_LAB_DEFAULT_PORT")

    # Gracefully stop any active session before killing Purple Lab
    curl -s --max-time 3 -X POST "http://127.0.0.1:${port}/api/session/stop" >/dev/null 2>&1 || true
    sleep 1

    # Try PID file first (check global location, then local). Identity is
    # verified with _loki_pid_looks_like_purplelab (anchored interpreter +
    # exact server_py path + uid match), not a loose comm-name substring: a
    # decoy or another installation's process merely named "python" would
    # otherwise pass the old check.
    local pid_file="$PURPLE_LAB_PID_FILE"
    if [ ! -f "$pid_file" ]; then
        pid_file="${LOKI_DIR}/purple-lab/purple-lab.pid"
    fi
    if [ -f "$pid_file" ]; then
        local pid
        pid=$(cat "$pid_file" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            if _loki_pid_looks_like_purplelab "$pid"; then
                kill "$pid" 2>/dev/null
                sleep 1
                if kill -0 "$pid" 2>/dev/null; then
                    kill -9 "$pid" 2>/dev/null
                fi
                echo -e "${GREEN}Purple Lab stopped (PID: $pid)${NC}"
                stopped=true
            else
                echo -e "${YELLOW}PID $pid is not this installation's Purple Lab process, skipping${NC}"
            fi
        fi
        rm -f "$PURPLE_LAB_PID_FILE"
    fi

    # Also kill any process on the port (catches orphans without PID file).
    # Same identity check as above: a port/pattern-derived PID must be
    # positively identified as OURS before it is signaled (D14/D15/D16 class).
    local port_pid
    port_pid=$(lsof -ti:"$port" -sTCP:LISTEN 2>/dev/null | head -1 || true)
    if [ -n "$port_pid" ]; then
        if _loki_pid_looks_like_purplelab "$port_pid"; then
            kill "$port_pid" 2>/dev/null
            sleep 1
            if kill -0 "$port_pid" 2>/dev/null; then
                kill -9 "$port_pid" 2>/dev/null
            fi
            if [ "$stopped" = false ]; then
                echo -e "${GREEN}Purple Lab stopped (orphan PID: $port_pid on port $port)${NC}"
            fi
            stopped=true
        else
            echo -e "${YELLOW}Port $port is held by a process that is not this installation's Purple Lab server, leaving it alone${NC}"
        fi
    fi

    # Also kill any orphan web-app/server.py processes (catches processes from
    # other CWDs). pgrep -f only ENUMERATES candidates by argv substring; each
    # candidate must then pass _loki_pid_looks_like_purplelab before being
    # killed. (The previous cwd-equality check here was dead in practice --
    # server.py never calls chdir, so its cwd is whatever launched it, not the
    # web-app dir -- and even when it matched, cwd alone is not identity: an
    # unrelated python process merely launched FROM that directory would also
    # pass. argv identity via the shared helper covers both gaps.)
    local orphan_pids
    orphan_pids=$(pgrep -f "web-app/server.py" 2>/dev/null || true)
    if [ -n "$orphan_pids" ]; then
        for opid in $orphan_pids; do
            if kill -0 "$opid" 2>/dev/null && _loki_pid_looks_like_purplelab "$opid"; then
                kill "$opid" 2>/dev/null
                sleep 1
                if kill -0 "$opid" 2>/dev/null; then
                    kill -9 "$opid" 2>/dev/null
                fi
                if [ "$stopped" = false ]; then
                    echo -e "${GREEN}Purple Lab stopped (orphan process PID: $opid)${NC}"
                fi
                stopped=true
            fi
        done
    fi

    if [ "$stopped" = false ]; then
        echo "Purple Lab is not running."
    fi

    # Clean PID files from current dir AND home dir (catches cross-CWD state)
    rm -f "$PURPLE_LAB_PID_FILE" "${LOKI_DIR}/purple-lab/port" 2>/dev/null || true
    rm -f "$HOME/.loki/purple-lab/purple-lab.pid" "$HOME/.loki/purple-lab/port" 2>/dev/null || true

    # Kill only processes that Purple Lab started (tracked in child-pids.json)
    # External loki sessions (started via "loki start" from terminal) are NOT touched
    local pids_file="${LOKI_DIR}/purple-lab/child-pids.json"
    if [ -f "$pids_file" ]; then
        local tracked_pids
        tracked_pids=$(LOKI_PIDS_FILE="$pids_file" python3 -c "import json, os; [print(p) for p in json.load(open(os.environ['LOKI_PIDS_FILE']))]" 2>/dev/null || true)
        if [ -n "$tracked_pids" ]; then
            echo "Cleaning up Purple Lab build processes..."
            for opid in $tracked_pids; do
                pkill -TERM -P "$opid" 2>/dev/null || true
                kill -TERM "$opid" 2>/dev/null || true
            done
            sleep 2
            for opid in $tracked_pids; do
                pkill -9 -P "$opid" 2>/dev/null || true
                kill -9 "$opid" 2>/dev/null || true
            done
            echo "Purple Lab build processes cleaned up."
        fi
        rm -f "$pids_file" 2>/dev/null || true
    fi

    # Companion cleanup: the dashboard server that the web UI runs alongside
    # is already stopped by cmd_dashboard_stop, called before this function by
    # cmd_web()'s "stop" case -- that path is PID-file-authoritative and is the
    # single source of truth for "our" dashboard. KNOWN GAP: cmd_dashboard_stop
    # (this file) verifies only liveness (`kill -0`), not process identity --
    # unlike autonomy/run.sh's loki_mark_project_stopped_and_maybe_kill_shared_dashboard,
    # which additionally calls _loki_pid_looks_like_dashboard to guard against a
    # recycled PID now naming something else. That hardening is not ported
    # here; out of scope for this fix, noted for a follow-up.
    #
    # A former dashboard-argv pgrep/port sweep here was DELETED rather than
    # hardened: any identity check reachable from argv alone (anchoring on
    # "-m dashboard.server", the module form cmd_dashboard_start launches) can
    # only prove "this is A loki dashboard process", never "this is the
    # dashboard THIS session started" -- a dashboard launched from a different
    # worktree, a different SKILL_DIR, or a concurrent CI shard passes that
    # check identically. Re-killing "a" dashboard here duplicates
    # cmd_dashboard_stop's job with a strictly weaker identity guarantee and
    # was, before this fix, actually vacuous (the case-statement re-tested the
    # exact same substrings pgrep had already matched on, and the SIGKILL pass
    # walked the raw unfiltered pgrep list with no check at all) -- see
    # docs/v10/DECISIONS.md D14/D15/D16.

    # FIX-563 (v7.30.0): do NOT blanket-kill loki-run-* orchestrators here.
    # The prior unscoped `pgrep -f "loki-run-..."` reaped EVERY orchestrator on
    # the machine, including foreign `loki start` sessions launched from other
    # terminals/CWDs that the web UI never owned. Purple Lab's own build
    # processes are already reaped authoritatively above via child-pids.json
    # (the only PIDs this session actually started). Foreign builds survive,
    # mirroring the cwd-scoped dashboard-stop pattern (FIX rationale: a user
    # invoking "loki web stop" expects the web UI gone, not their other builds).

    # Clean up all PID files globally
    rm -f "${LOKI_DIR}/dashboard/dashboard.pid" 2>/dev/null || true
    rm -f "$HOME/.loki/dashboard/dashboard.pid" 2>/dev/null || true
    rm -f "${PURPLE_LAB_STATE_DIR}/purple-lab.pid" "${PURPLE_LAB_STATE_DIR}/port" 2>/dev/null || true
}

cmd_web_status() {
    local port
    port=$(cat "${PURPLE_LAB_STATE_DIR}/port" 2>/dev/null || cat "${LOKI_DIR}/purple-lab/port" 2>/dev/null || echo "$PURPLE_LAB_DEFAULT_PORT")

    # Resolve log file path (check home-based state dir first, then CWD-based)
    local log_path="${PURPLE_LAB_STATE_DIR}/logs/purple-lab.log"
    if [ ! -f "$log_path" ]; then
        log_path="${LOKI_DIR}/purple-lab/logs/purple-lab.log"
    fi

    # Check PID file
    if [ -f "$PURPLE_LAB_PID_FILE" ]; then
        local pid
        pid=$(cat "$PURPLE_LAB_PID_FILE" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            echo -e "${GREEN}Purple Lab is running${NC}"
            echo "  PID:  $pid"
            echo "  URL:  http://${PURPLE_LAB_DEFAULT_HOST}:${port}"
            echo "  Logs: $log_path"
            return 0
        fi
        rm -f "$PURPLE_LAB_PID_FILE"
    fi

    # Fallback: check port directly (catches orphans)
    local port_pid
    port_pid=$(lsof -ti:"$port" -sTCP:LISTEN 2>/dev/null | head -1 || true)
    if [ -n "$port_pid" ]; then
        echo -e "${YELLOW}Purple Lab is running (orphan process)${NC}"
        echo "  PID:  $port_pid"
        echo "  Port: $port"
        echo "  Note: No PID file found. Stop with: loki web stop"
        return 0
    fi

    echo "Purple Lab is not running."
}

# Read app-runner state.json and echo url, status, port (one per line, in that
# order). Shared by the plain preview browser path and the --public tunnel path
# so there is no parse drift between them. Path is passed as argv (not inline
# interpolation) so a path with quotes cannot break the script. Always exits 0;
# callers re-split with sed -n '1p/2p/3p'. Logic is byte-identical to the
# original inline parse in cmd_preview (do not "clean up" -- behavior-neutral).
_read_app_state() {
    local state_file="$1"
    local url status port parsed
    if command -v python3 &> /dev/null; then
        parsed=$(python3 -c "import json,sys
try:
    d=json.load(open(sys.argv[1]))
    print(d.get('url',''))
    print(d.get('status',''))
    print(d.get('port',''))
except Exception:
    print('');print('');print('')" "$state_file" 2>/dev/null)
        url=$(printf '%s\n' "$parsed" | sed -n '1p')
        status=$(printf '%s\n' "$parsed" | sed -n '2p')
        port=$(printf '%s\n' "$parsed" | sed -n '3p')
    else
        url=$(grep -oE '"url"[[:space:]]*:[[:space:]]*"[^"]*"' "$state_file" 2>/dev/null | head -1 | sed 's/.*"url"[[:space:]]*:[[:space:]]*"//;s/"$//')
        status=$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$state_file" 2>/dev/null | head -1 | sed 's/.*"status"[[:space:]]*:[[:space:]]*"//;s/"$//')
        port=$(grep -oE '"port"[[:space:]]*:[[:space:]]*[0-9]+' "$state_file" 2>/dev/null | head -1 | grep -oE '[0-9]+$')
    fi
    printf '%s\n%s\n%s\n' "$url" "$status" "$port"
}

# Pure extractor: pull the first cloudflared quick-tunnel URL out of a log file.
# Reads a FILE (not a live process) so it is unit-testable. Echoes the URL or
# nothing. Always returns 0 -- a no-match must not abort the caller under
# `set -o pipefail` (grep exits 1 on no match, so the pipeline is `|| true`d).
_extract_tunnel_url_cloudflared() {
    local logfile="${1:-}"
    [ -f "$logfile" ] || return 0
    grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' "$logfile" 2>/dev/null | head -1 || true
}

# Pure extractor: pull the public_url out of an ngrok 4040 API JSON dump.
# Reads a FILE so it is unit-testable. Prefers the https tunnel. Uses python3
# json (path passed as argv -- no shell interpolation into the heredoc), with a
# grep fallback if python3 is absent or the parse fails. Echoes URL or nothing;
# always returns 0.
_extract_tunnel_url_ngrok() {
    local json_file="${1:-}"
    [ -f "$json_file" ] || return 0
    local out=""
    if command -v python3 >/dev/null 2>&1; then
        out=$(python3 - "$json_file" <<'PYEXTRACT' 2>/dev/null || true
import json, sys
try:
    with open(sys.argv[1]) as fh:
        data = json.load(fh)
except Exception:
    sys.exit(0)
tunnels = data.get("tunnels", []) if isinstance(data, dict) else []
urls = [t.get("public_url", "") for t in tunnels if isinstance(t, dict) and t.get("public_url")]
https = [u for u in urls if u.startswith("https://")]
chosen = https[0] if https else (urls[0] if urls else "")
if chosen:
    print(chosen)
PYEXTRACT
)
    fi
    if [ -z "$out" ]; then
        # Grep fallback: take https public_url values first, else any public_url.
        out=$(grep -oE '"public_url"[[:space:]]*:[[:space:]]*"https://[^"]*"' "$json_file" 2>/dev/null | head -1 | grep -oE 'https://[^"]*' || true)
        if [ -z "$out" ]; then
            out=$(grep -oE '"public_url"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_file" 2>/dev/null | head -1 | sed 's/.*"public_url"[[:space:]]*:[[:space:]]*"//;s/"$//' || true)
        fi
    fi
    printf '%s' "$out"
}

# Teardown for a launched tunnel: TERM, brief grace, then KILL, then remove the
# log. All steps are `|| true` so the EXIT/INT/TERM trap is set -e safe and never
# leaves an orphaned public tunnel. Args: tunnel_pid [logfile].
_preview_public_teardown() {
    local tunnel_pid="${1:-}"
    local logfile="${2:-}"
    if [ -n "$tunnel_pid" ]; then
        kill -TERM "$tunnel_pid" 2>/dev/null || true
        sleep 1
        kill -KILL "$tunnel_pid" 2>/dev/null || true
    fi
    if [ -n "$logfile" ]; then
        rm -f "$logfile" 2>/dev/null || true
    fi
}

# Expose the already-running local app over a PUBLIC URL by wrapping the user's
# OWN tunnel CLI (cloudflared or ngrok). Consent-gated, default-OFF. Loki never
# proxies traffic and never bundles a binary. Args: provider yes no_host_rewrite.
# Returns non-zero on any precondition failure or declined/refused consent
# (except an interactive decline, which is a clean exit 0).
_preview_public() {
    local provider="${1:-}"
    local assume_yes="${2:-false}"
    local no_host_rewrite="${3:-false}"

    local state_file="${LOKI_DIR}/app-runner/state.json"

    # Precondition 1: state.json must exist.
    if [ ! -f "$state_file" ]; then
        echo "No app running. Nothing to expose." >&2
        echo "Start a build (loki start) or check status (loki status) first." >&2
        return 1
    fi

    # Read url/status/port via the shared helper (same parse as plain preview).
    local _state url status port
    _state=$(_read_app_state "$state_file")
    url=$(printf '%s\n' "$_state" | sed -n '1p')
    status=$(printf '%s\n' "$_state" | sed -n '2p')
    port=$(printf '%s\n' "$_state" | sed -n '3p')

    # Precondition 2: status must be running.
    if [ "$status" != "running" ]; then
        echo "App is not running (status: ${status:-unknown}). Refusing to expose." >&2
        echo "The app runner starts the app after a successful build iteration." >&2
        return 1
    fi

    # Precondition 3: resolve URL/port (fallback mirrors the plain path).
    if [ -z "$url" ]; then
        url="http://localhost:${port:-3000}"
    fi
    local probe_port="${port:-3000}"

    # Precondition 4: PORT must actually be reachable. Never tunnel a dead port.
    # Reuses the curl-readiness poll pattern (autonomy/loki:4979).
    local retries=0
    local port_ready=false
    while [ $retries -lt 6 ]; do
        if curl -s "http://localhost:${probe_port}" > /dev/null 2>&1; then
            port_ready=true
            break
        fi
        sleep 0.5
        retries=$((retries + 1))
    done
    if [ "$port_ready" != true ]; then
        echo "Port ${probe_port} is not responding. Refusing to expose a dead port." >&2
        echo "Confirm the app is up locally (loki preview) before sharing it." >&2
        return 1
    fi

    # Consent (default-OFF). Print the full warning every time (even with --yes).
    echo "WARNING: This makes the app running on THIS machine reachable by ANYONE who has"
    echo "the URL, over the public internet, using YOUR tunnel account."
    echo "- The app may have NO authentication. Anyone with the link can use it."
    echo "- Traffic flows through your own cloudflared/ngrok account, not through Loki."
    echo "- This stays up until you stop it. Stop it when you are done."
    echo ""

    if [ "$assume_yes" = true ]; then
        : # --yes: warning printed above; skip the prompt.
    elif [ -t 0 ]; then
        # Interactive TTY. Default-N: only ^[Yy] proceeds. (Deliberately NOT the
        # default-Y idiom at :1894 -- public exposure is unsafe by default.)
        local confirm=""
        echo -e "Expose this app publicly? [y/N] \c"
        read -r confirm || true
        if [[ ! "$confirm" =~ ^[Yy] ]]; then
            echo "Aborted. App was not exposed."
            return 0
        fi
    else
        # Non-TTY without --yes: never silently expose.
        echo "Refusing to expose a public tunnel non-interactively without --yes." >&2
        return 1
    fi

    # === SEAM (Agent B): provider detection + tunnel launch + URL extraction ===

    # Provider detection (SS5). command -v based so a PATH stub works in tests.
    # If $provider was requested, require exactly that one; else prefer
    # cloudflared (no account needed for quick tunnels) then ngrok.
    local chosen_provider=""
    if [ -n "$provider" ]; then
        # Allowlist: only cloudflared/ngrok are supported. Without this guard an
        # arbitrary on-PATH binary name (e.g. --provider ls) would set
        # chosen_provider and fall through to the ngrok launch branch below.
        case "$provider" in
            cloudflared|ngrok) ;;
            *)
                echo -e "${RED}Unsupported tunnel provider: ${provider}${NC}" >&2
                echo "Supported providers: cloudflared, ngrok" >&2
                return 1
                ;;
        esac
        if command -v "$provider" >/dev/null 2>&1; then
            chosen_provider="$provider"
        fi
    else
        if command -v cloudflared >/dev/null 2>&1; then
            chosen_provider="cloudflared"
        elif command -v ngrok >/dev/null 2>&1; then
            chosen_provider="ngrok"
        fi
    fi

    if [ -z "$chosen_provider" ]; then
        # Honest install hint (mirrors the gh-missing block). Never pretend
        # success, never download a binary. Loki wraps YOUR OWN client.
        if [ -n "$provider" ]; then
            echo -e "${RED}Requested tunnel provider not found: ${provider}${NC}" >&2
        else
            echo -e "${RED}No tunnel CLI found (cloudflared or ngrok)${NC}" >&2
        fi
        echo "" >&2
        echo "Loki wraps YOUR OWN tunnel client. It never downloads or bundles one." >&2
        echo "Install one of the following, then re-run:" >&2
        echo "" >&2
        echo "  cloudflared (no account needed for quick tunnels):" >&2
        echo "    brew install cloudflared                        # macOS" >&2
        echo "    https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/  # all platforms" >&2
        echo "" >&2
        echo "  ngrok (needs a free authtoken):" >&2
        echo "    brew install ngrok                              # macOS" >&2
        echo "    https://ngrok.com/download                      # all platforms" >&2
        return 1
    fi

    # State dir, parallel to app-runner/.
    mkdir -p "${LOKI_DIR}/preview" 2>/dev/null || true
    local log_file="${LOKI_DIR}/preview/${chosen_provider}.log"

    # Build the launch command (SS7/SS8). Host-header rewrite is default-ON to
    # fix the #1 dev-server "Invalid Host header" failure; --no-host-rewrite
    # opts out.
    local -a tunnel_cmd=()
    if [ "$chosen_provider" = "cloudflared" ]; then
        tunnel_cmd=(cloudflared tunnel --url "http://localhost:${probe_port}")
        if [ "$no_host_rewrite" != true ]; then
            tunnel_cmd+=(--http-host-header localhost)
        fi
    else
        # ngrok
        tunnel_cmd=(ngrok http "${probe_port}")
        if [ "$no_host_rewrite" != true ]; then
            tunnel_cmd+=(--host-header=rewrite)
        fi
    fi

    echo ""
    echo "Starting ${chosen_provider} tunnel for http://localhost:${probe_port} ..."

    # Launch redirected to a log, in the background; capture its pid.
    "${tunnel_cmd[@]}" > "$log_file" 2>&1 &
    local tunnel_pid=$!

    # Install teardown immediately so no exit path leaves an orphaned tunnel.
    trap '_preview_public_teardown "$tunnel_pid" "$log_file"' EXIT INT TERM

    # Poll for the public URL. cloudflared writes it to its log; ngrok exposes
    # it via the local 4040 API. Bounded loop (~20 x sleep 0.5 = ~10s). Also
    # detect early tunnel-process death so we fail honestly instead of hanging.
    local public_url=""
    local tries=0
    while [ $tries -lt 20 ]; do
        # Early death detection: if the tunnel process is gone, report and bail.
        if ! kill -0 "$tunnel_pid" 2>/dev/null; then
            echo -e "${RED}${chosen_provider} exited; see ${log_file}${NC}" >&2
            # Surface the tail of the log for context.
            tail -n 10 "$log_file" 2>/dev/null >&2 || true
            if [ "$chosen_provider" = "ngrok" ]; then
                echo "If this is an auth error, add your token: ngrok config add-authtoken <token>" >&2
            fi
            # The EXIT trap fires at SHELL exit, not function return, so reap the
            # process now and clear the trap. Keep the log for inspection (the
            # message points at it), so omit the log arg to teardown.
            _preview_public_teardown "$tunnel_pid"
            trap - EXIT INT TERM
            return 1
        fi

        if [ "$chosen_provider" = "cloudflared" ]; then
            public_url=$(_extract_tunnel_url_cloudflared "$log_file")
        else
            local api_json="${LOKI_DIR}/preview/ngrok-api.json"
            if curl -s "http://127.0.0.1:4040/api/tunnels" -o "$api_json" 2>/dev/null; then
                public_url=$(_extract_tunnel_url_ngrok "$api_json")
            fi
            rm -f "$api_json" 2>/dev/null || true
        fi

        if [ -n "$public_url" ]; then
            break
        fi
        sleep 0.5
        tries=$((tries + 1))
    done

    if [ -z "$public_url" ]; then
        echo -e "${RED}Timed out waiting for a public URL from ${chosen_provider}.${NC}" >&2
        tail -n 10 "$log_file" 2>/dev/null >&2 || true
        if [ "$chosen_provider" = "ngrok" ]; then
            echo "ngrok may be missing an authtoken: ngrok config add-authtoken <token>" >&2
        fi
        # Reap now (the EXIT trap fires only at shell exit, not function return,
        # which would orphan the tunnel back in cmd_preview/main). Keep the log
        # for inspection, so omit the log arg.
        _preview_public_teardown "$tunnel_pid"
        trap - EXIT INT TERM
        return 1
    fi

    # URL captured. Print it clearly and re-print the live warning.
    echo ""
    echo -e "${GREEN}Public preview URL:${NC} ${public_url}"
    echo ""
    echo "WARNING: This URL is live and reachable by ANYONE who has it, over the"
    echo "public internet, through YOUR ${chosen_provider} account. The app may have no"
    echo "authentication. This stays up until you stop it."
    echo ""
    echo "Press Ctrl+C to stop sharing."

    # Foreground-block on the tunnel. Ctrl+C delivers INT -> trap -> teardown.
    # `wait` returns 130 on signal; guard so set -e does not abort the clean exit.
    wait "$tunnel_pid" 2>/dev/null || true

    # Idempotent final teardown (the INT trap may already have run) + remove the
    # log on this success/Ctrl+C path. Clear the trap so it does not re-fire at
    # shell exit with out-of-scope locals.
    _preview_public_teardown "$tunnel_pid" "$log_file"
    trap - EXIT INT TERM

    echo ""
    echo "Tunnel stopped."
    return 0
}

# Open the running app preview (the app Loki built and started locally).
# Surfaces the existing app-runner state; does not start or change the app.
cmd_preview() {
    local open_browser=true
    local public=false
    local provider=""
    local assume_yes=false
    local no_host_rewrite=false

    # Parse options. Loop so flag combos like `--public --yes` work; preserve
    # the existing leniency on unknown args (no hard error -> no behavior drift).
    while [ $# -gt 0 ]; do
        case "${1:-}" in
            --help|-h|help)
                echo -e "${BOLD}Loki Mode -- open the running app preview${NC}"
                echo ""
                echo "Usage: loki preview [--no-open]"
                echo "       loki preview --public [--provider cloudflared|ngrok] [--yes] [--no-host-rewrite]"
                echo "       loki open      (alias)"
                echo ""
                echo "Prints the URL of the app Loki built and started locally, then"
                echo "opens it in your browser. The app runner starts the app after the"
                echo "first successful build iteration. This serves a real local build"
                echo "from localhost on your machine; it is not hosted."
                echo ""
                echo "Options:"
                echo "  --no-open          Print the URL and status only; do not open a browser"
                echo "  --public           Expose the running app over a PUBLIC URL via your own"
                echo "                     tunnel CLI (cloudflared or ngrok). Consent-gated,"
                echo "                     default-OFF. Loki never proxies traffic or bundles a"
                echo "                     binary; it wraps YOUR OWN tunnel client."
                echo "  --provider NAME    Tunnel provider for --public: cloudflared or ngrok"
                echo "                     (default: auto-detect, cloudflared first)"
                echo "  --yes              Skip the --public consent prompt (warning is still"
                echo "                     printed). Required to use --public non-interactively"
                echo "  --no-host-rewrite  Do not rewrite the Host header on the tunnel"
                echo "  --help, -h         Show this help and exit"
                echo ""
                echo "WARNING (--public): exposes THIS machine's app to ANYONE with the URL,"
                echo "over the public internet, through your own tunnel account. The app may"
                echo "have no authentication. It stays up until you stop it."
                return 0
                ;;
            --no-open)
                open_browser=false
                ;;
            --public)
                public=true
                ;;
            --provider)
                # Guard against a missing OR flag-shaped value: an unguarded
                # shift when --provider is the last arg underflows under set -e,
                # and a bare "--provider --public" would otherwise swallow the
                # next flag as the provider name.
                case "${2:-}" in
                    -*|"")
                        echo "loki preview --provider requires a value (cloudflared|ngrok)" >&2
                        return 1
                        ;;
                    *)
                        provider="$2"
                        shift
                        ;;
                esac
                ;;
            --yes)
                assume_yes=true
                ;;
            --no-host-rewrite)
                no_host_rewrite=true
                ;;
        esac
        shift
    done

    # --public branches into the tunnel path BEFORE the browser-open logic.
    # Capture the exit code (set -e safe: a bare non-zero return on the call
    # line would trip set -e before `return` runs).
    if [ "$public" = true ]; then
        local rc=0
        _preview_public "$provider" "$assume_yes" "$no_host_rewrite" || rc=$?
        return $rc
    fi

    local state_file="${LOKI_DIR}/app-runner/state.json"
    if [ ! -f "$state_file" ]; then
        echo "No app running. The app runner starts after the first successful build iteration."
        echo "Run 'loki status' to check the current run."
        return 0
    fi

    # Parse url/status/port via the shared helper, then re-split (same logic the
    # inline parse used; behavior-neutral for the plain preview path).
    local url status port _state
    _state=$(_read_app_state "$state_file")
    url=$(printf '%s\n' "$_state" | sed -n '1p')
    status=$(printf '%s\n' "$_state" | sed -n '2p')
    port=$(printf '%s\n' "$_state" | sed -n '3p')

    if [ "$status" != "running" ]; then
        echo "App is not running (status: ${status:-unknown})."
        echo "The app runner starts the app after a successful build iteration."
        return 0
    fi

    if [ -z "$url" ]; then
        url="http://localhost:${port:-3000}"
    fi

    echo -e "${GREEN}Live app:${NC} $url  [running, port ${port:-?}]"
    echo "Served from localhost on this machine."

    if [ "$open_browser" = true ]; then
        loki_open_url "$url" 2>/dev/null || echo "Please open in browser: $url"
    fi
}

# =============================================================================
# loki deploy -- ADVISORY / PRINT-ONLY deploy command (FEAT-DEPLOY).
#
# WHY ITS OWN COMMAND (not a preview flag): `loki preview` is "show me the local
# app I already built and started" and is GATED on a running app (state.json
# status=running, a live reachable port). `loki deploy` is conceptually distinct:
# it is a STATIC, FILESYSTEM-ONLY advisory about the project's type and the user's
# installed cloud CLI / CI-CD pipeline. It must work with nothing running and no
# build started, so it has NO running-app precondition. Folding it into preview
# would force preview's running-app gate onto a feature that must not have one.
#
# HARD INVARIANT (DEPLOY-PLAN LOCK 5 + BRANCH-LIFECYCLE LOCK B4): PRINT-ONLY BY
# DEFAULT. Without --execute, cmd_deploy NEVER runs a cloud CLI
# (vercel/netlify/flyctl/wrangler) -- not even `--version`. Tool detection is
# `command -v` ONLY. Loki advises; the human runs the printed command.
#
# --execute (added for receipt-gated deploy) is the ONLY path that runs a deploy
# command, and it is gated on an Evidence Receipt that VERIFIES this exact tree
# (see the RECEIPT-GATED EXECUTION block below). The invariants that survive
# unconditionally, with or without --execute:
#   - NEVER `git push`, NEVER `gh pr create`. Out of scope entirely.
#   - NEVER a destructive cloud operation (delete/destroy/teardown/drop),
#     refused even with a perfect receipt.
#   - NEVER reads or forwards cloud credentials. The deploy command inherits the
#     user's already-authenticated CLI session; Loki does not touch credential
#     files or secret-bearing env vars.
# Only the clipboard tools (pbcopy/wl-copy/...), `command -v`, and -- under
# --execute alone -- the gated deploy command itself may run.
#
# This whole block is contiguous (helpers + cmd_deploy) so a test can extract it
# by name anchor, mirroring tests/test-preview-public.sh.
# =============================================================================

# _deploy_detect_type <dir>
# Echoes the PRIMARY project-type label (one of: nextjs, static, docker, node,
# python) or empty if none detected. Read-only file/dir existence + grep on
# package.json. Always returns 0 (caller treats empty as "no project"). First
# match wins for the primary label; cmd_deploy still offers multiple provider
# options per type. set -e safe: every grep is `|| true`-guarded.
_deploy_detect_type() {
    local dir="${1:-.}"
    local pkg="$dir/package.json"

    # 1. Next.js -- source signal ("next" dep or a next.config.*), NOT the build
    #    artifact (.next/standalone), since the build may not have run yet.
    if [ -f "$pkg" ] && grep -q '"next"' "$pkg" 2>/dev/null; then
        printf '%s' "nextjs"; return 0
    fi
    if [ -f "$dir/next.config.js" ] || [ -f "$dir/next.config.mjs" ] || [ -f "$dir/next.config.ts" ]; then
        printf '%s' "nextjs"; return 0
    fi

    # 2. Static / SPA -- a built dist/ or build/ dir containing index.html, OR a
    #    Vite / CRA source signal in package.json.
    if { [ -d "$dir/dist" ] && [ -f "$dir/dist/index.html" ]; } || \
       { [ -d "$dir/build" ] && [ -f "$dir/build/index.html" ]; }; then
        printf '%s' "static"; return 0
    fi
    if [ -f "$pkg" ] && { grep -q '"vite"' "$pkg" 2>/dev/null || grep -q '"react-scripts"' "$pkg" 2>/dev/null; }; then
        printf '%s' "static"; return 0
    fi

    # 3. Dockerfile / containerized server.
    if [ -f "$dir/Dockerfile" ]; then
        printf '%s' "docker"; return 0
    fi

    # 4. Generic Node server -- package.json with a start (or dev) script.
    if [ -f "$pkg" ] && { grep -q '"start"' "$pkg" 2>/dev/null || grep -q '"dev"' "$pkg" 2>/dev/null; }; then
        printf '%s' "node"; return 0
    fi

    # 5. Python.
    if [ -f "$dir/requirements.txt" ] || [ -f "$dir/pyproject.toml" ]; then
        printf '%s' "python"; return 0
    fi

    printf '%s' ""
    return 0
}

# _deploy_options_for_type <type>
# Pure, DIR-BLIND helper: echoes the ordered candidate rows for a project type,
# one per line, as `provider|cli|command|docs`. Idiomatic provider first per
# DEPLOY-PLAN LOCK 2. Lists the FULL ordered candidate set for the type
# UNCONDITIONALLY (no dir awareness, no CLI-installed filtering -- cmd_deploy does
# the `command -v` filtering). The command strings are the EXACT canonical forms
# from DEPLOY-PLAN LOCK 3 (a wrong flag is worse than no feature; do not
# paraphrase). Project-dependent dirs stay as `<...>` placeholders the user fills.
# Note: the Fly CLI binary is `flyctl` but the canonical deploy verb is
# `fly deploy` -- "fly" not "flyctl" here is CORRECT, intentional, not a typo.
# Row format: provider|cli|command|docs|rollback. rollback is a documented verb
# (vercel rollback, fly releases rollback <captured-version>) or `manual:<note>`
# when the provider documents no CLI rollback.
_deploy_options_for_type() {
    local type="${1:-}"
    case "$type" in
        nextjs)
            printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli|vercel rollback"
            printf '%s\n' "Netlify|netlify|netlify deploy --prod|https://docs.netlify.com/cli/get-started/|manual:Netlify documents no CLI rollback; restore the previous deploy from the Netlify dashboard"
            # fly not flyctl - correct: binary is flyctl, deploy verb is `fly deploy`.
            printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
            ;;
        static)
            printf '%s\n' "Netlify|netlify|netlify deploy --prod --dir=<build-output>|https://docs.netlify.com/cli/get-started/|manual:Netlify documents no CLI rollback; restore the previous deploy from the Netlify dashboard"
            printf '%s\n' "Cloudflare Pages|wrangler|wrangler pages deploy <build-output>|https://developers.cloudflare.com/workers/wrangler/install-and-update/|manual:Cloudflare rollback is not automated here; roll back from the dashboard or with the provider CLI yourself"
            printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli|vercel rollback"
            ;;
        docker)
            # fly not flyctl - correct (see note above).
            printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
            printf '%s\n' "Cloudflare|wrangler|wrangler deploy|https://developers.cloudflare.com/workers/wrangler/install-and-update/|manual:Cloudflare rollback is not automated here; roll back from the dashboard or with the provider CLI yourself"
            ;;
        node)
            # fly not flyctl - correct (see note above).
            printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
            printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli|vercel rollback"
            ;;
        python)
            # fly not flyctl - correct (see note above).
            printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
            ;;
    esac
    return 0
}

# _deploy_detect_cicd <dir>
# Returns 0 + echoes the detected CI/CD system name(s) (space-separated) if ANY
# pipeline config exists (BRANCH-LIFECYCLE LOCK B1); returns 1 + echoes nothing
# if none. Read-only file existence only. The `[ -e "$f" ]` guard inside the glob
# loop makes the no-match literal-glob case set -e safe (an unmatched glob expands
# to the literal pattern, which `[ -e ]` then rejects without aborting).
_deploy_detect_cicd() {
    local dir="${1:-.}"
    local found=""
    local systems=""

    # GitHub Actions: any .yml or .yaml under .github/workflows/.
    local f
    for f in "$dir"/.github/workflows/*.yml "$dir"/.github/workflows/*.yaml; do
        if [ -e "$f" ]; then found=1; systems="${systems} GitHub-Actions"; break; fi
    done

    [ -f "$dir/.gitlab-ci.yml" ] && { found=1; systems="${systems} GitLab-CI"; }
    [ -f "$dir/Jenkinsfile" ] && { found=1; systems="${systems} Jenkins"; }
    [ -f "$dir/.circleci/config.yml" ] && { found=1; systems="${systems} CircleCI"; }
    [ -f "$dir/azure-pipelines.yml" ] && { found=1; systems="${systems} Azure-Pipelines"; }
    [ -f "$dir/bitbucket-pipelines.yml" ] && { found=1; systems="${systems} Bitbucket-Pipelines"; }

    if [ -n "$found" ]; then
        # Trim the leading space and echo the names.
        printf '%s' "${systems# }"
        return 0
    fi
    printf '%s' ""
    return 1
}

# _deploy_copy_clipboard <cmd>
# Best-effort clipboard copy of a SINGLE command (DEPLOY-PLAN LOCK 4). TTY-gated,
# command-v guarded, ALWAYS returns 0 and never fatal. Echoes a confirmation note
# only if a copy tool actually ran. These clipboard tools ARE allowed to run; only
# the four cloud CLIs are forbidden.
_deploy_copy_clipboard() {
    local cmd="${1:-}"
    [ -n "$cmd" ] || return 0
    [ -t 1 ] || return 0
    local copied=""
    if command -v pbcopy >/dev/null 2>&1; then
        printf '%s' "$cmd" | pbcopy >/dev/null 2>&1 && copied="1" || true
    elif command -v wl-copy >/dev/null 2>&1; then
        printf '%s' "$cmd" | wl-copy >/dev/null 2>&1 && copied="1" || true
    elif command -v xclip >/dev/null 2>&1; then
        printf '%s' "$cmd" | xclip -selection clipboard >/dev/null 2>&1 && copied="1" || true
    elif command -v xsel >/dev/null 2>&1; then
        printf '%s' "$cmd" | xsel --clipboard --input >/dev/null 2>&1 && copied="1" || true
    elif command -v clip >/dev/null 2>&1; then
        printf '%s' "$cmd" | clip >/dev/null 2>&1 && copied="1" || true
    fi
    if [ -n "$copied" ]; then
        printf '%s\n' "  (copied to clipboard: ${cmd})"
    fi
    return 0
}

# _deploy_print_install_hint <type>
# Honest install hints (brew + official URL) for the candidate providers of a
# type. NEVER fabricates success, NEVER downloads a binary. Mirrors the
# tunnel-missing / gh-missing block. Caller redirects to stderr.
_deploy_print_install_hint() {
    local type="${1:-}"
    echo "No deploy CLI found for this ${type} project."
    echo "Loki never accesses your cloud account or runs deploy for you -- you run"
    echo "the printed command. Install one of the following, then re-run 'loki deploy':"
    echo ""
    local options provider cli command docs _rb
    options="$(_deploy_options_for_type "$type")"
    while IFS='|' read -r provider cli command docs _rb; do
        [ -n "$provider" ] || continue
        case "$cli" in
            vercel)   echo "  Vercel:      brew install vercel       |  ${docs}" ;;
            netlify)  echo "  Netlify:     brew install netlify-cli  |  ${docs}" ;;
            flyctl)   echo "  Fly.io:      brew install flyctl       |  ${docs}" ;;
            wrangler) echo "  Cloudflare:  npm i -g wrangler         |  ${docs}" ;;
            *)        echo "  ${provider}:  ${docs}" ;;
        esac
    done <<EOF
$options
EOF
    return 0
}

# _deploy_print_cloud_options <dir> <type> <do_clip> <hint_on_none>
# Prints every installed (project-type x CLI) option block, idiomatic first.
# Best-effort copies the FIRST installed command when do_clip=true. Returns 0 if
# at least one CLI was printed. If NONE installed: when hint_on_none=true, prints
# the honest install hint (to stderr) and returns 1; when false (pipeline path,
# where cloud is secondary), prints nothing and returns 1 silently.
_deploy_print_cloud_options() {
    local dir="${1:-.}"
    local type="${2:-}"
    local do_clip="${3:-true}"
    local hint_on_none="${4:-true}"

    local options=""
    options="$(_deploy_options_for_type "$type")"

    local printed=false
    local first_cmd=""
    local provider cli command docs _rb
    while IFS='|' read -r provider cli command docs _rb; do
        [ -n "$cli" ] || continue
        if command -v "$cli" >/dev/null 2>&1; then
            if [ "$printed" != true ]; then
                echo -e "${BOLD}Detected ${type} project. Deploy options (run one yourself):${NC}"
                echo ""
                printed=true
            fi
            echo "  ${provider}:"
            echo "    ${command}"
            echo "    docs: ${docs}"
            echo ""
            if [ -z "$first_cmd" ]; then
                first_cmd="$command"
            fi
        fi
    done <<EOF
$options
EOF

    if [ "$printed" = true ]; then
        # This sentence is TRUE only on the print-only path. Under --execute the
        # very next thing that happens may be the deploy running, and printing
        # "Loki does not deploy for you" immediately before Loki deploys is
        # exactly the kind of false statement this feature exists to not make.
        # _LOKI_DEPLOY_EXECUTING is set by cmd_deploy for this one decision.
        if [ "${_LOKI_DEPLOY_EXECUTING:-false}" = true ]; then
            echo "Reviewing this command against the evidence gate (--execute)."
        else
            echo "Loki does not deploy for you. Review, then run the command yourself."
        fi
        if [ "$do_clip" = true ] && [ -n "$first_cmd" ]; then
            _deploy_copy_clipboard "$first_cmd"
        fi
        return 0
    fi

    # No installed CLI for this type.
    if [ "$hint_on_none" = true ]; then
        _deploy_print_install_hint "$type" >&2
        return 1
    fi
    return 1
}

# =============================================================================
# RECEIPT-GATED EXECUTION (--execute). Everything below is INERT unless the user
# passes --execute on THIS invocation; the default path stays print-only.
#
# The design idea: a deploy is an action a VERIFIED RECEIPT AUTHORIZES. Not
# "let the agent run gcloud" -- the gate is the evidence artifact we already
# ship, and it must hold on every one of four independent checks.
#
# FAIL CLOSED IS THE WHOLE CONTRACT. Every helper here answers "" or a REFUSE
# reason when it cannot evaluate its check. An unevaluated gate is not a pass:
# no receipt, no python3, no gpg, a non-repo dir and an unresolvable anchor all
# REFUSE. That direction is deliberate -- a gate that cannot tell you whether
# the tree was verified must not deploy it.
# =============================================================================

# _deploy_receipt_verdict <proof.json>
# Echoes exactly one of VERIFIED | UNSIGNED | UNCHECKED | TAMPERED, the same four
# verdicts loki_remote_verify_receipt reports (see its header at loki:756).
# The vocabulary is REUSED, not reinvented, because a second set of words for
# the same four states is how they get collapsed into two.
#
# Deliberately NOT loki_remote_verify_receipt itself: that function returns 0 for
# UNSIGNED, which is right for its caller (submit) and wrong here (integrity is
# not provenance). We read the same signed/hash_ok/gpg_ok triple and let the
# CALLER decide which verdicts authorize.
#
# hash_ok is read, NOT the top-level `ok` and NOT the exit code: `ok` folds in
# tree_drift, and proof-verify.py exits 1 on drift alone. Gating on either would
# refuse every honest receipt whose tree moved -- the false-alarm direction
# docs/VERIFICATION-COST.md documents. Anything unreadable is TAMPERED-or-worse,
# so the fallback verdict is UNCHECKED (never a pass).
_deploy_receipt_verdict() {
    local pj="$1"
    local verifier="${_LOKI_SCRIPT_DIR}/lib/proof-verify.py"

    [ -f "$pj" ] || { printf '%s' "UNCHECKED"; return 0; }
    if [ ! -f "$verifier" ] || ! command -v python3 >/dev/null 2>&1; then
        printf '%s' "UNCHECKED"; return 0
    fi

    local out hash_ok _vroot
    _vroot="$(loki_verify_root 2>/dev/null)" || { printf '%s' "UNCHECKED"; return 0; }
    out="$(python3 -I -S "$verifier" "$pj" "$_vroot" 2>/dev/null || true)"

    # "The verifier did not RUN" and "the verifier says the hash is bad" are
    # different facts and must not collapse. If they do, a broken python3
    # produces the verdict TAMPERED -- an accusation of forgery for a check that
    # never executed. That is the same error the signed path is careful to avoid
    # with a missing key: it sends a user hunting a forgery that never
    # happened, and trains them to shrug at a real TAMPERED. Unparseable output
    # is UNCHECKED, which still REFUSES -- fail-closed, honestly labelled.
    if [ -z "$out" ] || ! printf '%s' "$out" | python3 -c 'import json,sys;json.load(sys.stdin)' >/dev/null 2>&1; then
        printf '%s' "UNCHECKED"; return 0
    fi

    hash_ok="$(printf '%s' "$out" | python3 -c 'import json,sys;print(str(json.load(sys.stdin).get("hash_ok",False)).lower())' 2>/dev/null || printf 'false')"
    if [ "$hash_ok" != "true" ]; then printf '%s' "TAMPERED"; return 0; fi

    # Provenance is the Ed25519 attestation, checked against the PUBLIC keys we
    # hold: the local signing key plus LOKI_RECEIPT_RETIRED_PUBKEYS. A legacy
    # gpg_signature is ignored: old receipts read UNSIGNED here. A token whose
    # kid is not in that set (another machine, a rotated-away key) cannot be
    # evaluated, so no key set is written and the verdict is UNCHECKED, never
    # TAMPERED: the hash is fine, we just cannot vouch for who signed it. A
    # local key only proves "a holder of this key", not an independent build.
    # Token SHAPE before any key lookup (mirrors engine10 checkAttestation): a present
    # attestation that is not a string, not a 3-part JWS, or whose header/payload are not
    # JSON with alg EdDSA is junk, so TAMPERED, never UNCHECKED, with or without a key.
    if ! python3 -E - "$pj" >/dev/null 2>&1 <<'PYSHAPE'
import sys
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, base64
def dec(x):
    return json.loads(base64.urlsafe_b64decode(x + "=" * (-len(x) % 4)))
v = json.load(open(sys.argv[1])).get("verification") or {}
t = v.get("attestation")
if t is None or t == "":
    sys.exit(0)
if not isinstance(t, str):
    sys.exit(1)
parts = t.split(".")
if len(parts) != 3 or not all(parts):
    sys.exit(1)
try:
    h = dec(parts[0]); dec(parts[1])
except Exception:
    sys.exit(1)
sys.exit(0 if isinstance(h, dict) and h.get("alg") == "EdDSA" else 1)
PYSHAPE
    then printf '%s' "TAMPERED"; return 0; fi

    local jwks att
    jwks="$(mktemp "${TMPDIR:-/tmp}/loki-jwks.XXXXXX")" || { printf '%s' "UNCHECKED"; return 0; }
    python3 -E - "${_LOKI_SCRIPT_DIR}" "$pj" >"$jwks" 2>/dev/null <<'PY' || true
import sys, json
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
sys.path.insert(0, sys.argv[1])
from receipt_jwt import load_signing_key, load_retired_public_keys, build_jwks, _b64url_decode
k, _ = load_signing_key(auto_generate=False)
jwks = build_jwks(k, load_retired_public_keys())
try:
    tok = json.load(open(sys.argv[2]))["verification"]["attestation"]
    kid = json.loads(_b64url_decode(tok.split(".")[0])).get("kid", "")
except Exception:
    kid = ""          # malformed or absent: let the checker decide (none / bad)
if not kid or kid in [j.get("kid") for j in jwks["keys"]]:
    print(json.dumps(jwks))
PY
    att="$(loki_proof_attestation_check "$pj" "$jwks")"
    rm -f "$jwks"
    case "$att" in
        ok)   printf '%s' "VERIFIED" ;;
        none) printf '%s' "UNSIGNED" ;;
        bad)  printf '%s' "TAMPERED" ;;
        *)    printf '%s' "UNCHECKED" ;;
    esac
    return 0
}

# _deploy_anchor_ok <proof.json> <dir>
# Echoes "" when the receipt anchors to the tree in front of us, else a REFUSE
# reason. Two checks, and BOTH are load-bearing:
#
#   1. resolve_anchor (autonomy/lib/outcome_ledger.py:122) -- sha algebra proving
#      base..head is reachable and produces exactly the receipt's file set.
#   2. head_sha == live HEAD.
#
# Check 2 is NOT redundant, and this was MEASURED rather than assumed: on a
# fixture where the tree advanced one commit past the receipt, resolve_anchor
# still returned ("anchored", None) -- correctly, because it answers "can I
# follow this receipt to measure outcomes", not "does this receipt describe the
# tree I am about to ship". Without check 2 a receipt for an ANCESTOR commit
# authorizes deploying code it never verified, which is the exact failure this
# feature exists to prevent.
_deploy_anchor_ok() {
    local pj="$1" dir="${2:-.}"
    command -v python3 >/dev/null 2>&1 || { printf '%s' "anchor could not be evaluated (python3 unavailable)"; return 0; }
    git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || { printf '%s' "anchor could not be evaluated (not a git repository)"; return 0; }

    local live_head
    live_head="$(git -C "$dir" rev-parse HEAD 2>/dev/null || printf '')"
    [ -n "$live_head" ] || { printf '%s' "anchor could not be evaluated (no HEAD commit)"; return 0; }

    # _LOKI_SCRIPT_DIR is a plain shell var, so it is passed EXPLICITLY here --
    # relying on it being exported would silently fall back to a bare "lib" path
    # and report every receipt as unevaluable.
    PJ="$pj" LIVE_HEAD="$live_head" LOKI_LIB="${_LOKI_SCRIPT_DIR}/lib" \
        python3 - "$dir" <<'PYANCHOR' 2>/dev/null || printf '%s' "anchor could not be evaluated (anchor check failed to run)"
import json, os, sys
sys.dont_write_bytecode = True
sys.path.insert(0, os.environ.get("LOKI_LIB", ""))
try:
    from outcome_ledger import resolve_anchor
except Exception:
    print("anchor could not be evaluated (outcome_ledger unavailable)", end="")
    raise SystemExit(0)
d = json.load(open(os.environ["PJ"]))
git = (d.get("facts") or {}).get("git") or {}
base, head = git.get("base_sha") or "", git.get("head_sha") or ""
files = git.get("diff") or d.get("files_changed") or []
if not isinstance(files, list):
    files = []
state, reason = resolve_anchor(base, head, files, sys.argv[1])
if state != "anchored":
    # TWO DIFFERENT FACTS, and collapsing them makes this gate unusable.
    #
    # Measured on this repository: 8 of 9 receipts carry NO facts.git.base_sha,
    # which matches the "0 of 9 anchored" figure already published in
    # docs/VERIFICATION-COST.md. Refusing both cases identically means the gate
    # rejects ~89% of real receipts on anchoring alone -- and a gate that always
    # refuses teaches users to reach for an override, which is worse than no
    # gate at all.
    #
    #   UNANCHORED  no baseline was recorded. We can prove neither match nor
    #               mismatch. Refuse by DEFAULT, but let the caller override
    #               per invocation, and record that nothing was proven.
    #   MISMATCH    a baseline WAS recorded and it does not resolve to this
    #               tree. That is positive evidence of the wrong build, and no
    #               override may bypass it.
    #
    # The override covers "unknown", never "known wrong". Emitting a machine
    # readable prefix so the caller can tell the two apart without parsing prose.
    #
    # DISCRIMINATED ON THE REASON NAME, not on `not base`. MEASURED: the receipt
    # this repo's gate actually selects carries base_sha=4b825dc6 (the empty
    # tree), so `base` is TRUTHY and a `not base` test files it under MISMATCH --
    # the one case blocking the repo would stay unoverridable. resolve_anchor
    # returns base_sha_empty (line 135) BEFORE the greenfield check (line 137),
    # so reaching greenfield_no_baseline proves base_sha was non-empty.
    #
    # Exactly two reasons mean "no baseline was recorded", and together they
    # cover all 9 receipts here (8 base_sha_empty, 1 greenfield_no_baseline).
    # Every OTHER reason keeps refusing, including with the flag set:
    #   not_reachable_from_head, diff_range_mismatch -- positive evidence of a
    #     DIFFERENT change. Mismatch by construction.
    #   change_not_committed, sha_not_in_history, head_sha_empty, no_git -- the
    #     check could not be evaluated, and this gate's stated doctrine is that
    #     an unevaluable check is a refusal, not a pass.
    if reason in ("base_sha_empty", "greenfield_no_baseline"):
        print("UNANCHORED:receipt records no baseline (%s), so it cannot be tied "
              "to this tree" % reason, end="")
    else:
        print("receipt does not anchor to this tree (%s)" % (reason or "unresolvable"), end="")
elif head != os.environ["LIVE_HEAD"]:
    # Anchored but STALE: the receipt verified an ancestor, not this tree.
    print("receipt records a different commit (%s) than HEAD (%s)"
          % (head[:12], os.environ["LIVE_HEAD"][:12]), end="")
PYANCHOR
    return 0
}

# _deploy_is_destructive <command>
# Returns 0 (true) if the command names a destructive cloud operation. Refused
# even under --execute with a perfect receipt: a receipt authorizes shipping the
# verified tree, never tearing an environment down. Word-boundary matched so
# "wrangler pages deploy dist" is not caught by "destroy" appearing in a path.
_deploy_is_destructive() {
    # Allowlisted rollback verbs (the only ones auto-rollback may run) are never
    # destructive: they restore a prior release rather than tear anything down.
    printf '%s' "${1:-}" | grep -Eq '^(vercel rollback|fly releases rollback [0-9]+)$' && return 1
    printf '%s' "${1:-}" | grep -Eqi '(^|[^[:alnum:]_-])(delete|destroy|teardown|down|drop|rm|remove|purge|prune|wipe|scale-down)([^[:alnum:]_-]|$)'
}

# _deploy_tree_clean <dir>
# Echoes "" when the working tree is clean, else a REFUSE reason. A dirty tree
# means the bytes about to ship are NOT the bytes that were verified, which is
# precisely the failure the receipt gate exists to prevent. A non-repo is
# UNEVALUABLE, not clean -- it lands in REFUSE with a named reason.
_deploy_tree_clean() {
    local dir="${1:-.}"
    git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || { printf '%s' "working tree could not be evaluated (not a git repository)"; return 0; }
    local st rc=0
    st="$(git -C "$dir" status --porcelain 2>/dev/null)" || rc=$?
    # A git that could not report is UNEVALUABLE, never "clean" and never
    # "dirty": naming it dirty invents a specific finding out of a failed
    # measurement, and the user would go looking for edits that do not exist.
    [ "$rc" -eq 0 ] || { printf '%s' "working tree could not be evaluated (git status failed)"; return 0; }
    [ -z "$st" ] || printf '%s' "working tree is dirty ($(printf '%s\n' "$st" | grep -c '') uncommitted change(s))"
    return 0
}

# _deploy_latest_receipt <dir>
# Echoes the newest .loki/proofs/<id>/proof.json path, or "" if none exists.
# Newest-by-name: proof ids are timestamp-prefixed, so lexical order is
# chronological. Absence is not an error here; the caller turns "" into the
# named REFUSE reason "no receipt".
_deploy_latest_receipt() {
    local dir="${1:-.}"
    local proofs="$dir/${LOKI_DIR:-.loki}/proofs"
    # LOKI_DIR may already be absolute (it is elsewhere in this CLI); prefer it
    # directly when it resolves, else fall back to the dir-relative form.
    [ -d "${LOKI_DIR:-}/proofs" ] && proofs="${LOKI_DIR}/proofs"
    [ -d "$proofs" ] || { printf '%s' ""; return 0; }
    local newest=""
    local d
    for d in "$proofs"/*/; do
        [ -f "$d/proof.json" ] || continue
        newest="$d/proof.json"
    done
    printf '%s' "$newest"
    return 0
}

# _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha] [destination] [deployed_url] [health_url_source] [env_keys_csv] [env_source] [domain] [domain_status] [tls_status] [tls_not_after] [domain_provenance] [live_binding]
# Writes the executed deploy's own record, then signs it (loki.deploy/1, Ed25519 via the
# receipt signer in loki-ts/src/util/receipt_signer.ts) so it is bound to the receipt that
# authorized it; `loki verify <record>` re-checks it offline. An unsigned record is reported
# loudly and verifies as UNCHECKED, never VERIFIED.
# Writes the executed deploy's own record. A deploy with no record of itself
# defeats the entire premise of gating on evidence.
#
# NOT written into .loki/proofs/: a hand-written JSON there carries no integrity
# hash, so `loki proof verify` would read it as a TAMPERED receipt -- a
# self-inflicted false alarm. It lives in .loki/deploys/ where it cannot be
# mistaken for a verifiable receipt.
_deploy_record() {
    local dir="${1:-.}" command="$2" rc="$3" run_id="$4" anchor="$5"
    # WHICH of the three applied: anchored (the receipt was tied to this tree)
    # or unanchored_overridden (it was not, and the operator said go anyway).
    # "refused" needs no value: a refusal never reaches this function, so the
    # existence of a record already means one of the two above.
    local anchor_state="${6:-anchored}"
    # Post-deploy health and rollback facts. "unchecked" never implies healthy.
    local health_status="${7:-unchecked}" health_url="${8:-}"
    local rb_attempted="${9:-false}" rb_exit="${10:-}"
    local receipt_sha="${11:-}" started_at="${12:-}" finished_at="${13:-}" target="${14:-}" canary="${15:-false}"
    local post_rb="${16:-unchecked}" hcode_rec="${17:-}" hmatch="${18:-}" tree_sha="${19:-}" dest_rec="${20:-}"
    local deployed_url="${21:-}" hsrc="${22:-}"
    # --env-file: key NAMES only (comma-joined, validated identifiers) and the file
    # path. A value is never passed here, so the signed record cannot carry one.
    local env_keys_csv="${23:-}" env_source="${24:-}"
    # --domain: tls_status is "unchecked" unless the openssl proof ran and said otherwise.
    local domain_rec="${25:-}" domain_status="${26:-}" tls_status="${27:-unchecked}" tls_not_after="${28:-}" domain_prov="${29:-}"
    local live_binding="${30:-}"
    local ddir="$dir/.loki/deploys"
    [ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
    mkdir -p "$ddir" 2>/dev/null || return 0
    local f="$ddir/$(date -u +%Y%m%dT%H%M%SZ)-$$.json"
    DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
    DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
    DR_RBA="$rb_attempted" DR_RBX="$rb_exit" DR_RSHA="$receipt_sha" DR_T0="$started_at" \
    DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" DR_PRB="$post_rb" DR_HC="$hcode_rec" DR_HM="$hmatch" DR_TREE="$tree_sha" DR_DEST="$dest_rec" DR_DURL="$deployed_url" DR_HSRC="$hsrc" \
    DR_ENVK="$env_keys_csv" DR_ENVS="$env_source" \
    DR_DOM="$domain_rec" DR_DOMS="$domain_status" DR_TLS="$tls_status" DR_TLSA="$tls_not_after" DR_DOMP="$domain_prov" DR_LB="$live_binding" \
        python3 -c '
import json, os, datetime
state = os.environ.get("DR_STATE") or "anchored"
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
json.dump({
    "schema": "loki.deploy/1",
    "receipt_sha256": os.environ.get("DR_RSHA", ""),
    "head_sha": os.environ["DR_ANCHOR"],
    "target": os.environ.get("DR_TGT", ""),
    "started_at": os.environ.get("DR_T0") or now,
    "finished_at": os.environ.get("DR_T1") or now,
    "canary": os.environ.get("DR_CAN") == "true",
    "recorded_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    "command": os.environ["DR_CMD"],
    "exit_code": int(os.environ["DR_RC"]),
    "authorizing_run_id": os.environ["DR_RUN"],
    "anchor_sha": os.environ["DR_ANCHOR"],
    "anchor_state": state,
    # Spelled out so a reader months later does not have to know the vocabulary
    # to judge how strong the authorization was.
    "anchor_proven": state == "anchored",
    "health_status": os.environ.get("DR_HS") or "unchecked",
    "health_url": os.environ.get("DR_HU") or "",
    "rollback_attempted": os.environ.get("DR_RBA") == "true",
    "rollback_exit": int(os.environ["DR_RBX"]) if os.environ.get("DR_RBX", "") != "" else None,
    "post_rollback_health": os.environ.get("DR_PRB") or "unchecked",
    "health_match": os.environ.get("DR_HM", ""),
    "tree_sha": os.environ.get("DR_TREE", ""),
    "destination": os.environ.get("DR_DEST", ""),
    "deployed_url": os.environ.get("DR_DURL", ""),
    "health_url_source": os.environ.get("DR_HSRC", ""),
    "env_keys": sorted(k for k in os.environ.get("DR_ENVK", "").split(",") if k),
    "env_source": os.environ.get("DR_ENVS", ""),
    "domain": os.environ.get("DR_DOM", ""),
    "domain_status": os.environ.get("DR_DOMS", ""),
    "tls_status": os.environ.get("DR_TLS") or "unchecked",
    "tls_not_after": os.environ.get("DR_TLSA", ""),
    "domain_provenance": os.environ.get("DR_DOMP", ""),
    "live_binding": os.environ.get("DR_LB") or "unchecked",
    "health_http_code": int(os.environ["DR_HC"]) if os.environ.get("DR_HC", "").isdigit() else None,
}, open(os.environ["DR_OUT"], "w"), indent=2)
' 2>/dev/null || return 0
    # Sign through the one Bun implementation. A subshell, because the delegate execs.
    if ! ( _loki_bun_delegate engine10 verify --sign-deploy "$f" ) >/dev/null 2>&1; then
        echo -e "${YELLOW}WARNING: the deploy record is UNSIGNED (no signing key or Bun route); it verifies as UNCHECKED.${NC}" >&2
    fi
    printf '%s' "$f"
    return 0
}

# _deploy_first_installed_command <dir> <type>
# Echoes the idiomatic deploy command for the FIRST installed CLI of this type,
# or "" if none is installed. Same ordering and same canonical strings
# _deploy_print_cloud_options prints, so --execute can never run a command that
# differs from the one the advisory showed. `command -v` only; no CLI is run.
_deploy_first_installed_command() {
    local dir="${1:-.}" type="${2:-}"
    local options provider cli command docs _rb
    options="$(_deploy_options_for_type "$type")"
    while IFS='|' read -r provider cli command docs _rb; do
        [ -n "$cli" ] || continue
        if command -v "$cli" >/dev/null 2>&1; then
            printf '%s' "$command"
            return 0
        fi
    done <<EOF
$options
EOF
    printf '%s' ""
    return 0
}

# _deploy_rollback_for <type> <command>
# Echoes the rollback column of the first option row whose deploy command is
# <command>, or "" when none. Pure table lookup; runs nothing.
_deploy_rollback_for() {
    local type="${1:-}" want="${2:-}" provider cli command docs rb
    while IFS='|' read -r provider cli command docs rb; do
        [ -n "$cli" ] || continue
        if [ "$command" = "$want" ]; then
            printf '%s' "$rb"
            return 0
        fi
    done <<EOF
$(_deploy_options_for_type "$type")
EOF
    printf '%s' ""
    return 0
}

# _deploy_fly_capture_version <dir>
# Echoes the newest Fly release version BEFORE a deploy so a rollback has a real
# target, or "" when it cannot be read (then no rollback is attempted).
_deploy_fly_capture_version() {
    local dir="${1:-.}" out
    out="$( cd "$dir" && fly releases --json 2>/dev/null )" || { printf '%s' ""; return 0; }
    printf '%s' "$out" | python3 -c '
import json, sys
try:
    rel = json.load(sys.stdin)
    v = rel[0].get("Version") if rel else None
    print(int(v) if v is not None else "", end="")
except Exception:
    print("", end="")
' 2>/dev/null || printf '%s' ""
    return 0
}

# _deploy_probe_health <url> [match]
# Bounded poll: echoes the last HTTP status, returns 0 on any 2xx. With a match
# string the 2xx body must also contain it (fixed-string), else the attempt fails
# and the status is still echoed. Attempts and
# spacing are env-tunable (LOKI_DEPLOY_HEALTH_ATTEMPTS / _INTERVAL) for tests.
_deploy_probe_health() {
    local url="${1:-}" match="${2:-}" attempts="${LOKI_DEPLOY_HEALTH_ATTEMPTS:-5}" interval="${LOKI_DEPLOY_HEALTH_INTERVAL:-3}"
    local i=0 code="000" body="/dev/null"
    if [ -n "$match" ]; then body="$(mktemp 2>/dev/null)" || body="/dev/null"; fi
    case "$attempts" in ''|*[!0-9]*) attempts=5 ;; esac
    case "$interval" in ''|*[!0-9]*) interval=3 ;; esac
    while [ "$i" -lt "$attempts" ]; do
        i=$((i + 1))
        code="$(curl -s -o "$body" -w '%{http_code}' --max-time 10 "$url" 2>/dev/null)" || code="000"
        case "$code" in
            2??)
                if [ -z "$match" ] || grep -qF -- "$match" "$body" 2>/dev/null; then
                    [ "$body" = /dev/null ] || rm -f "$body"
                    printf '%s' "$code"; return 0
                fi
                ;;
        esac
        [ "$i" -lt "$attempts" ] && [ "$interval" -gt 0 ] && sleep "$interval"
    done
    [ "$body" = /dev/null ] || rm -f "$body"
    printf '%s' "$code"
    return 1
}

# _deploy_provenance_root <dir> <type>
# Echoes the static root that ships with the deploy and may carry /.well-known/, or "" when no
# such root exists. nextjs and Vite: public/. Built static: the detected dist/ then build/.
# docker, python and node serve no static root, so they print "" and are never bound.
_deploy_provenance_root() {
    local dir="${1:-.}" type="${2:-}"
    case "$type" in
        nextjs) printf '%s' "$dir/public" ;;
        static)
            if [ -f "$dir/dist/index.html" ]; then printf '%s' "$dir/dist"
            elif [ -f "$dir/build/index.html" ]; then printf '%s' "$dir/build"
            elif [ -f "$dir/package.json" ] && grep -q '"vite"' "$dir/package.json" 2>/dev/null; then printf '%s' "$dir/public"
            fi
            ;;
    esac
    return 0
}

# _deploy_clear_provenance <dir>
# Removes .well-known/loki-provenance.json from EVERY candidate static root (public/, dist/,
# build/). Runs before each deploy decides whether to write a fresh doc, so a doc left by an
# earlier deploy can never ship with a build it does not describe (unanchored override,
# unsupported type, signing failure, or a root that moved between deploys).
_deploy_clear_provenance() {
    local dir="${1:-.}" r
    for r in public dist build; do
        rm -f "$dir/$r/.well-known/loki-provenance.json" 2>/dev/null || true
    done
    return 0
}

# _deploy_write_provenance <dir> <root> <head_sha> <tree_sha> <receipt_sha256> <run_id>
# Writes <root>/.well-known/loki-provenance.json and signs it in place (loki verify
# --sign-provenance). Called only after the gate (including the dirty-tree check) passed, so the
# untracked file never dirties the tree that was verified; it is also added to .git/info/exclude
# so the NEXT deploy's dirty-tree gate does not trip over it. Returns non-zero when not signed.
_deploy_write_provenance() {
    local dir="$1" root="$2" head="$3" tree="$4" rsha="$5" rid="$6"
    local f="$root/.well-known/loki-provenance.json" ex
    rm -f "$f" 2>/dev/null
    [ -n "$head" ] && [ -n "$tree" ] && [ -n "$rsha" ] && [ -n "$rid" ] || return 1
    mkdir -p "$root/.well-known" 2>/dev/null || return 1
    PV_H="$head" PV_T="$tree" PV_R="$rsha" PV_I="$rid" PV_OUT="$f" python3 -c '
import json, os, datetime
json.dump({"schema": "loki.provenance/1", "head_sha": os.environ["PV_H"], "tree_sha": os.environ["PV_T"],
           "receipt_sha256": os.environ["PV_R"], "run_id": os.environ["PV_I"],
           "built_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")},
          open(os.environ["PV_OUT"], "w"), indent=2)
' 2>/dev/null || { rm -f "$f"; return 1; }
    ex="$(git -C "$dir" rev-parse --git-path info/exclude 2>/dev/null)" || ex=""
    if [ -n "$ex" ]; then
        case "$ex" in /*) ;; *) ex="$dir/$ex" ;; esac
        grep -qxF '**/.well-known/loki-provenance.json' "$ex" 2>/dev/null \
            || { mkdir -p "$(dirname "$ex")" 2>/dev/null; printf '%s\n' '**/.well-known/loki-provenance.json' >> "$ex" 2>/dev/null; } || true
    fi
    if ! ( cd "$dir" && _loki_bun_delegate engine10 verify --sign-provenance "$f" ) >/dev/null 2>&1; then
        rm -f "$f"
        return 1
    fi
    return 0
}

# _deploy_probe_live_binding <dir> <url> <anchor>
# Fetches <url>/.well-known/loki-provenance.json through loki verify --url --expect-sha.
# Returns 0 VERIFIED (signature holds and head_sha equals the anchor), 1 MISMATCH (a doc
# was served but is forged or names a different SHA, still so on the last bounded retry),
# 2 UNCHECKED (no doc could be fetched or its key is unknown, after a bounded retry with the
# health-probe tunables). Never VERIFIED on 2.
_deploy_probe_live_binding() {
    local dir="$1" url="$2" anchor="$3"
    local attempts="${LOKI_DEPLOY_HEALTH_ATTEMPTS:-5}" interval="${LOKI_DEPLOY_HEALTH_INTERVAL:-3}" i=0 vrc=2
    case "$attempts" in ''|*[!0-9]*) attempts=5 ;; esac
    case "$interval" in ''|*[!0-9]*) interval=3 ;; esac
    while [ "$i" -lt "$attempts" ]; do
        i=$((i + 1))
        vrc=0
        ( cd "$dir" && _loki_bun_delegate engine10 verify --url "$url" --expect-sha "$anchor" ) >/dev/null 2>&1 || vrc=$?
        [ "$vrc" -eq 0 ] && return 0
        [ "$i" -lt "$attempts" ] && [ "$interval" -gt 0 ] && sleep "$interval"
    done
    # A mismatch on the last attempt is FAILED; earlier mismatches may be propagation of the previous build.
    [ "$vrc" -eq 1 ] && return 1
    return 2
}

# _deploy_policy_refusal <dir>
# Echoes a refusal reason when the policy file does not permit a deploy, else "".
# No policy file: "" (unchanged behaviour). File present and node or check.js
# unavailable: refuse (fail-closed, mirrors run.sh check_policy). Exit 0 allows;
# 2 (approval needed) and anything else refuse. Scoped env, no secrets forwarded.
_deploy_policy_refusal() {
    local dir="${1:-.}" gdir="${LOKI_DATA_DIR:-$HOME/.loki}"
    if [ ! -f "$dir/.loki/policies.json" ] && [ ! -f "$dir/.loki/policies.yaml" ] \
       && [ ! -f "$gdir/policies.json" ] && [ ! -f "$gdir/policies.yaml" ]; then
        printf '%s' ""
        return 0
    fi
    local checkjs="${SKILL_DIR:-}/src/policies/check.js"
    [ -f "$checkjs" ] || checkjs="${PROJECT_DIR:-.}/src/policies/check.js"
    if ! command -v node >/dev/null 2>&1; then
        printf '%s' "policy file present but node is unavailable: cannot evaluate policy (fail-closed)"
        return 0
    fi
    if [ ! -f "$checkjs" ]; then
        printf '%s' "policy file present but check.js is missing (fail-closed)"
        return 0
    fi
    local out rc=0
    out="$(env -i PATH="$PATH" HOME="$HOME" LOKI_PROJECT_DIR="$dir" \
        ${LOKI_DATA_DIR:+LOKI_DATA_DIR="$LOKI_DATA_DIR"} \
        node "$checkjs" pre_deployment '{"action":"deploy"}' 2>/dev/null)" || rc=$?
    if [ "$rc" -eq 0 ]; then
        # Exit 0 also means "nothing to evaluate". Deploy needs explicit permission:
        # the policy must set autonomy or actions.deploy, else refuse.
        local setsperm=""
        setsperm="$(env -i PATH="$PATH" HOME="$HOME" LOKI_PROJECT_DIR="$dir" \
            ${LOKI_DATA_DIR:+LOKI_DATA_DIR="$LOKI_DATA_DIR"} \
            node -e 'var E=require(process.argv[1].replace(/check\.js$/,"engine.js")).PolicyEngine;var e=new E(process.env.LOKI_PROJECT_DIR);var p=e._policies;p=p&&(p.policies||p);var a=p&&p.actions&&typeof p.actions==="object"?p.actions.deploy:undefined;process.stdout.write(p&&(p.autonomy!==undefined||a!==undefined)?"yes":"");try{e.destroy()}catch(x){}process.exit(0)' "$checkjs" 2>/dev/null)" || setsperm=""
        if [ "$setsperm" != "yes" ]; then
            printf '%s' "policy file sets no autonomy/actions.deploy for deploy (no policy permission)"
        else
            printf '%s' ""
        fi
        return 0
    fi
    local why
    why="$(printf '%s' "$out" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("reason",""),end="")' 2>/dev/null || printf '')"
    if [ "$rc" -eq 2 ]; then
        printf '%s' "policy requires approval for deploy: ${why:-no reason given}"
    else
        printf '%s' "policy denied deploy: ${why:-policy check failed (fail-closed)}"
    fi
    return 0
}

# _deploy_staging_receipt_ok <dir> <sha>
# Returns 0 only when a staging deploy record for exactly <sha> exists in
# .loki/deploys/, exited 0, did not fail its health probe, and `loki verify`
# reports VERIFIED. Anything that cannot be evaluated is "no" (fail closed).
_deploy_staging_receipt_ok() {
    local dir="${1:-.}" sha="${2:-}" ddir="$1/.loki/deploys" f
    [ -n "$sha" ] || return 1
    [ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
    [ -d "$ddir" ] || return 1
    for f in "$ddir"/*.json; do
        [ -f "$f" ] || continue
        python3 -c 'import json,sys
r=json.load(open(sys.argv[1]))
ok=r.get("schema")=="loki.deploy/1" and r.get("destination")=="staging" and r.get("anchor_sha")==sys.argv[2] and r.get("exit_code")==0 and r.get("health_status")!="FAILED_HEALTH"
sys.exit(0 if ok else 1)' "$f" "$sha" 2>/dev/null || continue
        ( _loki_bun_delegate engine10 verify "$f" 2>&1 ) | grep -q "^verdict: VERIFIED" && return 0
    done
    return 1
}

# _deploy_env_load <env_file> <example_file>
# Parses KEY=VALUE lines with NO shell evaluation (python, never source/eval) and
# writes NUL-terminated records to stdout: one "KEY=VALUE" per key (sorted by
# key), then "__LOKI_ENV_OK__". On any problem it writes a single
# "__LOKI_ENV_ERR__:<reason>" record instead. A reason names keys and line
# numbers only, never a value. When <example_file> exists, every key it lists
# must be present in <env_file>, else the reason names the missing keys.
_deploy_env_load() {
    python3 - "$1" "$2" <<'PYEOF'
import os, re, sys
KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
def out(s):
    sys.stdout.buffer.write(s.encode("utf-8", "surrogateescape") + b"\0")
def parse(path, label):
    found = {}
    with open(path, encoding="utf-8", errors="surrogateescape") as fh:
        for n, raw in enumerate(fh, 1):
            line = raw.strip()
            if not line or line.startswith("#"):
                continue
            if line.startswith("export "):
                line = line[7:].lstrip()
            if "=" not in line:
                raise ValueError("%s line %d is not KEY=VALUE" % (label, n))
            k, v = line.split("=", 1)
            k, v = k.strip(), v.strip()
            if not KEY.match(k):
                raise ValueError("%s line %d has an invalid key name" % (label, n))
            if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
                v = v[1:-1]
            elif " #" in v:
                v = v.split(" #", 1)[0].rstrip()
            if "\0" in v:
                raise ValueError("%s key %s has a NUL byte in its value" % (label, k))
            found[k] = v
    return found
try:
    env = parse(sys.argv[1], "env file")
    if not env:
        raise ValueError("env file has no KEY=VALUE entries")
    if os.path.isfile(sys.argv[2]):
        missing = sorted(set(parse(sys.argv[2], ".env.example")) - set(env))
        if missing:
            raise ValueError("env file is missing key(s) listed in .env.example: " + ", ".join(missing))
except (OSError, ValueError) as e:
    out("__LOKI_ENV_ERR__:" + (str(e) if isinstance(e, ValueError) else "cannot read the env file"))
    sys.exit(0)
for k in sorted(env):
    out(k + "=" + env[k])
out("__LOKI_ENV_OK__")
PYEOF
}

# _deploy_domain_step <dir> <command> <domain> <deployed_url> <head_sha>
# --domain: attach <domain> with the provider's documented verb, then PROVE TLS with
# openssl. Sets _DD_STATUS (attached|manual|failed), _DD_TLS (valid|expiring|invalid|
# unchecked), _DD_NOTAFTER and _DD_PROV (match|mismatch|unchecked). Returns 6 when the
# attach failed or the TLS handshake/cert is invalid, else 0. tls_status is "valid" ONLY
# when the handshake gave a cert and 'openssl x509 -checkend 604800' accepted it.
_deploy_domain_step() {
    local dir="$1" command="$2" domain="$3" durl="$4" head="$5"
    local arc=0 hint="" pem="" xout="" xrc=0 tmo="" na="" body="" psha=""
    _DD_STATUS="" _DD_TLS="unchecked" _DD_NOTAFTER="" _DD_PROV="unchecked"
    echo "Attaching domain: ${domain}"
    case "$command" in
        "vercel --prod")
            hint="Vercel needs DNS for ${domain}: A 76.76.21.21 (apex) or CNAME cname.vercel-dns.com"
            if [ -z "$durl" ]; then
                echo "  The deploy printed no URL, so 'vercel alias set' has nothing to alias." >&2
                _DD_STATUS=failed
            else
                _DD_STATUS=attached
                ( cd "$dir" && vercel alias set "$durl" "$domain" ) || arc=$?
                [ "$arc" -eq 0 ] || _DD_STATUS=failed
            fi
            ;;
        "fly deploy")
            hint="Fly needs DNS for ${domain}: the A/AAAA or CNAME records 'fly certs show ${domain}' prints"
            _DD_STATUS=attached
            ( cd "$dir" && fly certs add "$domain" ) || arc=$?
            [ "$arc" -eq 0 ] || _DD_STATUS=failed
            ;;
        *)
            _DD_STATUS=manual
            hint="${command%% *} has no wired domain verb: add ${domain} in its dashboard, then point DNS at the record it shows"
            echo "  Manual step: ${hint}."
            ;;
    esac
    [ "$_DD_STATUS" != failed ] || echo -e "${RED}Domain attach failed (exit ${arc}).${NC}" >&2

    if ! command -v openssl >/dev/null 2>&1; then
        echo "TLS: openssl not found. tls_status=unchecked (never a claim of valid)."
    else
        command -v timeout >/dev/null 2>&1 && tmo="timeout 15"
        # shellcheck disable=SC2086
        pem="$($tmo openssl s_client -servername "$domain" -connect "${domain}:443" </dev/null 2>/dev/null)" || true
        case "$pem" in
            *"BEGIN CERTIFICATE"*)
                xout="$(printf '%s\n' "$pem" | openssl x509 -noout -enddate -checkend 604800 2>/dev/null)" || xrc=$?
                na="$(sed -n 's/^notAfter=//p' <<<"$xout" | head -n 1)"
                if [ -z "$na" ]; then _DD_TLS=invalid
                elif [ "$xrc" -eq 0 ]; then _DD_TLS=valid
                else _DD_TLS=expiring; fi
                [ -z "$na" ] || _DD_NOTAFTER="$(python3 -c 'import sys,datetime
try: print(datetime.datetime.strptime(sys.argv[1].strip(), "%b %d %H:%M:%S %Y GMT").strftime("%Y-%m-%dT%H:%M:%SZ"))
except Exception: print(sys.argv[1].strip())' "$na" 2>/dev/null || printf '%s' "$na")"
                ;;
            *) _DD_TLS=invalid ;;
        esac
        echo "TLS: ${domain} tls_status=${_DD_TLS}${_DD_NOTAFTER:+ (not after ${_DD_NOTAFTER})}"
        [ "$_DD_TLS" != expiring ] || echo -e "${YELLOW}  The certificate expires within 7 days.${NC}" >&2
    fi

    if [ "$_DD_TLS" = invalid ]; then
        echo -e "${RED}TLS invalid for ${domain}: no usable certificate was served. DNS may not point at the provider yet.${NC}" >&2
        [ -z "$hint" ] || echo "  ${hint}." >&2
    elif command -v curl >/dev/null 2>&1; then
        body="$(curl -fsS --max-time 5 "https://${domain}/.well-known/loki-provenance.json" 2>/dev/null)" || body=""
        psha="$(python3 -c 'import json,sys;print(json.loads(sys.stdin.read()).get("head_sha") or "")' <<<"$body" 2>/dev/null || true)"
        if [ -n "$psha" ] && [ -n "$head" ] && [ "$psha" = "$head" ]; then _DD_PROV=match
        elif [ -n "$psha" ]; then _DD_PROV=mismatch; echo -e "${YELLOW}Provenance head_sha ${psha:0:12} differs from the deployed ${head:0:12}.${NC}" >&2; fi
        echo "Provenance: ${_DD_PROV}"
    fi
    if [ "$_DD_STATUS" = failed ] || [ "$_DD_TLS" = invalid ]; then return 6; fi
    return 0
}

# _deploy_try_execute <dir> <type>
# The ONLY place a deploy command is ever run. Evaluates the gate, and either
# executes + records, or REFUSES naming every failed check. Returns the deploy
# command's exit code on execution, 1 on refusal.
#
# Ordering matters: the advisory has ALREADY been printed by the caller, so a
# refusal still leaves the user with exactly the output they get today.
_deploy_try_execute() {
    local dir="${1:-.}" type="${2:-}"
    # Set by cmd_deploy (dynamic scope); empty/false when called any other way.
    local health_url="${_LOKI_DEPLOY_HEALTH_URL:-}" auto_rollback="${_LOKI_DEPLOY_AUTO_ROLLBACK:-false}"
    local health_match="${_LOKI_DEPLOY_HEALTH_MATCH:-}"

    local command
    command="$(_deploy_first_installed_command "$dir" "$type")"
    if [ -z "$command" ]; then
        echo "" >&2
        echo -e "${RED}REFUSED: --execute found no installed deploy CLI for this ${type} project.${NC}" >&2
        echo "  Nothing was run. Install one of the CLIs listed above and re-run." >&2
        return 1
    fi

    # --canary uses only documented staged verbs; anything else refuses, naming why.
    local canary="${_LOKI_DEPLOY_CANARY:-false}" exec_cmd="$command" canary_kind=""
    if [ "$canary" = true ]; then
        case "$command" in
            "vercel --prod") canary_kind="vercel"; exec_cmd="vercel" ;;
            "fly deploy") canary_kind="fly"; exec_cmd="fly deploy --strategy canary" ;;
            *)
                echo -e "${RED}REFUSED: --canary is not supported for: ${command}${NC}" >&2
                echo "  This provider documents no staged rollout verb (Vercel: deploy then 'vercel promote'; Fly: 'fly deploy --strategy canary'). Nothing was run." >&2
                return 1
                ;;
        esac
    fi

    # preview/staging use a non-production verb; only Vercel documents one here.
    local dest="${_LOKI_DEPLOY_DESTINATION:-}"
    if [ "$dest" = preview ] || [ "$dest" = staging ]; then
        case "$command" in
            "vercel --prod") exec_cmd="vercel" ;;
            *)
                echo -e "${RED}REFUSED: --destination ${dest} is not supported for: ${command}${NC}" >&2
                echo "  No non-production verb is documented for this provider (Vercel: 'vercel' without --prod). Nothing was run." >&2
                return 1
                ;;
        esac
    fi

    # --env-file: values live ONLY in env_pairs and the provider argv. Stdout, the
    # 'Running:' line and the record carry names (K=***), never values.
    # Documented verbs only: Vercel '-e K=V' per key on the deploy itself; Fly
    # 'fly secrets set --stage K=V ...' before 'fly deploy'; Netlify
    # 'netlify env:import FILE' and Cloudflare 'wrangler secret bulk FILE' before
    # the deploy (the provider reads FILE, so no value enters argv).
    local env_file="${_LOKI_DEPLOY_ENV_FILE:-}" env_kind="" env_keys="" env_disp="" env_pre_disp="" _env_rec _env_ok=false _env_err=""
    local -a env_pairs=() env_pre_argv=()
    if [ -n "$env_file" ]; then
        case "$command" in
            "vercel --prod") env_kind="vercel" ;;
            "fly deploy") env_kind="fly" ;;
            "netlify deploy --prod") env_kind="netlify" ;;
            "wrangler deploy") env_kind="cloudflare" ;;
            *)
                echo -e "${RED}REFUSED: --env-file is not supported for: ${command}${NC}" >&2
                echo "  No documented env verb is wired for this provider. Nothing was run." >&2
                return 1
                ;;
        esac
        while IFS= read -r -d '' _env_rec; do
            case "$_env_rec" in
                __LOKI_ENV_OK__) _env_ok=true ;;
                __LOKI_ENV_ERR__:*) _env_err="${_env_rec#__LOKI_ENV_ERR__:}" ;;
                *) env_pairs+=("$_env_rec") ;;
            esac
        done < <(_deploy_env_load "$env_file" "$dir/.env.example")
        if [ "$_env_ok" != true ]; then
            echo -e "${RED}REFUSED: --env-file: ${_env_err:-the env file could not be parsed}${NC}" >&2
            echo "  Nothing was run." >&2
            return 1
        fi
        case "$env_kind" in
            fly) env_pre_argv=(fly secrets set --stage); env_pre_disp="fly secrets set --stage" ;;
            netlify) env_pre_argv=(netlify env:import "$env_file"); env_pre_disp="netlify env:import ${env_file}" ;;
            cloudflare) env_pre_argv=(wrangler secret bulk "$env_file"); env_pre_disp="wrangler secret bulk ${env_file}" ;;
        esac
        for _env_rec in "${env_pairs[@]}"; do
            env_keys="${env_keys:+${env_keys},}${_env_rec%%=*}"
            case "$env_kind" in
                vercel) env_disp="${env_disp} -e ${_env_rec%%=*}=***" ;;
                fly) env_pre_argv+=("$_env_rec"); env_pre_disp="${env_pre_disp} ${_env_rec%%=*}=***" ;;
            esac
        done
    fi

    local reasons
    reasons="$(_deploy_gate "$dir" "$command")"

    # Split the marker out BEFORE the emptiness test: it is a fact about how
    # the gate passed, never a reason it failed. Left in, it would refuse every
    # overridden deploy and print the marker as a reason.
    local anchor_state="anchored"
    case "$reasons" in
        *"$_DEPLOY_ANCHOR_MARKER"*)
            anchor_state="unanchored_overridden"
            # Shell substitution, NOT `grep -v`: under `set -e` a grep that
            # filters out every line exits 1 and aborts the deploy silently --
            # measured, and it looked exactly like the gate refusing.
            reasons="${reasons//$_DEPLOY_ANCHOR_MARKER/}"
            # Drop the blank line the removal left WITHOUT joining the real
            # reasons together: sed deletes empty lines only, so a refusal that
            # also carries other reasons still prints them one per line.
            reasons="$(printf '%s' "$reasons" | sed '/^[[:space:]]*$/d')"
            ;;
    esac

    if [ -n "$reasons" ]; then
        echo ""
        echo -e "${RED}REFUSED: not deploying. The evidence gate did not pass.${NC}" >&2
        echo "  Would have run: ${command}" >&2
        echo "" >&2
        # printf '%s\n', NOT '%s': command substitution strips the trailing
        # newline, and `read` then DISCARDS a final unterminated line -- so a
        # single-reason refusal printed no reasons at all. A refusal that names
        # nothing is the one thing this gate must never do.
        printf '%s\n' "$reasons" | while IFS= read -r r; do
            [ -n "$r" ] && echo "  - ${r}" >&2
        done
        echo "" >&2
        echo "  A check that could not be EVALUATED is a refusal, not a pass." >&2
        echo "  Nothing was executed. The advisory command above is unchanged." >&2
        return 1
    fi

    # Gate passed. Capture the authorizing receipt's identity for the record.
    local pj run_id anchor
    pj="$(_deploy_latest_receipt "$dir")"
    run_id="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("run_id") or "")' "$pj" 2>/dev/null || printf '')"
    anchor="$(git -C "$dir" rev-parse HEAD 2>/dev/null || printf '')"

    echo ""
    if [ "$anchor_state" = "unanchored_overridden" ]; then
        # "VERIFIED this tree" would be FALSE here: the receipt's checks passed,
        # but nothing tied it to the code about to ship. Saying so is the whole
        # point of allowing the override rather than weakening the gate.
        echo -e "${YELLOW}Evidence gate PASSED with the anchor UNPROVEN (--allow-unanchored).${NC}"
        echo "  Receipt ${run_id:-unknown} passed its own checks, but records no baseline,"
        echo "  so it was NOT tied to this tree (${anchor:0:12}). You authorized this."
    else
        echo -e "${GREEN}Evidence gate PASSED.${NC} Receipt ${run_id:-unknown} VERIFIED this tree (${anchor:0:12})."
    fi
    [ -n "$env_pre_disp" ] && echo "Running (env): ${env_pre_disp}"
    if [ "$canary" = true ]; then echo "Running (canary): ${exec_cmd}${env_disp}"; else echo "Running: ${command}${env_disp}"; fi
    echo ""

    # Run in the project dir. No credential env var is read, set, or forwarded:
    # the command inherits the user's already-authenticated CLI session as-is.
    #
    # The deploy runs as an argv array, never through eval: the canonical
    # command ("vercel --prod") is word-split from the hardcoded
    # _deploy_options_for_type table (already past the destructive-operation
    # check in _deploy_gate), and --env-file values are appended as single
    # array elements, so a value such as $(cmd) is never evaluated.
    # Resolve the rollback target BEFORE deploying: after it, the previous Fly
    # release is no longer the newest one and cannot be recovered.
    local rb_cmd="" rb_note=""
    if [ -n "$health_url" ]; then
        local rb_raw
        rb_raw="$(_deploy_rollback_for "$type" "$command")"
        case "$rb_raw" in
            manual:*) rb_note="${rb_raw#manual:}" ;;
            *"<captured-version>"*)
                local fv
                fv="$(_deploy_fly_capture_version "$dir")"
                if [ -n "$fv" ]; then rb_cmd="${rb_raw//<captured-version>/$fv}"
                else rb_note="could not read the current Fly release before deploying, so no rollback target is known"; fi
                ;;
            "") rb_note="no rollback verb is documented for this command" ;;
            *) rb_cmd="$rb_raw" ;;
        esac
    fi

    # Provenance doc: shipped with the build so the live URL can prove which SHA it serves.
    local live_binding="unchecked" prov_root="" prov_written=false
    prov_root="$(_deploy_provenance_root "$dir" "$type")"
    # Every branch below starts from no doc: only a doc written by THIS deploy may ship.
    _deploy_clear_provenance "$dir"
    if [ "$anchor_state" = "unanchored_overridden" ]; then
        live_binding="unanchored"
        echo "Live binding unanchored: the evidence gate was overridden with the anchor UNPROVEN, so no provenance doc is published and the live URL is not bound to ${anchor:0:12}." >&2
    elif [ -z "$prov_root" ]; then
        live_binding="unsupported"
        echo "Live binding unsupported: a ${type} project has no static root to serve /.well-known/loki-provenance.json, so the live URL is not bound to ${anchor:0:12}." >&2
    elif _deploy_write_provenance "$dir" "$prov_root" "$anchor" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)" \
        "$(python3 -c 'import json,sys;print((json.load(open(sys.argv[1])).get("verification") or {}).get("hash") or "")' "$pj" 2>/dev/null || true)" "$run_id"; then
        prov_written=true
        echo "Provenance written: ${prov_root}/.well-known/loki-provenance.json"
    else
        echo -e "${YELLOW}WARNING: the provenance doc could not be signed, so the live URL will not be bound to this SHA.${NC}" >&2
    fi

    local rc=0 started_at finished_at purl="" canary_out=""
    local -a deploy_argv=()
    read -r -a deploy_argv <<<"$exec_cmd"
    if [ "$env_kind" = vercel ]; then
        for _env_rec in "${env_pairs[@]}"; do deploy_argv+=(-e "$_env_rec"); done
    fi
    # What the record and messages name: the command with values masked as K=***.
    local exec_disp="${env_pre_disp:+${env_pre_disp}; }${exec_cmd}${env_disp}"
    if [ -n "$env_pre_disp" ]; then
        # The env step runs after the gate and before the deploy; if it fails the
        # deploy is NOT run (a deploy missing its env is not the one asked for).
        local env_rc=0
        ( cd "$dir" && "${env_pre_argv[@]}" ) || env_rc=$?
        if [ "$env_rc" -ne 0 ]; then
            echo -e "${RED}REFUSED: the env step exited ${env_rc}: ${env_pre_disp}${NC}" >&2
            echo "  The deploy was NOT run." >&2
            return "$env_rc"
        fi
    fi
    started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    local deployed_url="" health_src=""
    [ -n "$health_url" ] && health_src="flag"
    local health_status="unchecked" rb_attempted=false rb_exit="" hcode="" rb_hcode="" post_rb="unchecked"
    if [ "$canary_kind" = vercel ]; then
        # Staged: a preview deploy (no --prod), probe the preview URL, promote only if healthy.
        # Nothing reaches production before the probe, so there is nothing to roll back.
        rb_cmd=""; auto_rollback=false
        canary_out="$( cd "$dir" && "${deploy_argv[@]}" )" || rc=$?
        printf '%s\n' "$canary_out"
        purl="$(grep -Eo 'https?://[^[:space:]]+' <<<"$canary_out" | tail -n 1)"
        if [ "$rc" -eq 0 ] && [ -n "$purl" ]; then
            health_url="$purl"; deployed_url="$purl"; health_src="captured"
            echo "Probing canary health: ${health_url}"
            if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
                health_status="healthy"
                echo "Promoting: vercel promote ${purl}"
                exec_disp="${exec_disp}; vercel promote ${purl}"
                ( cd "$dir" && vercel promote "$purl" ) || rc=$?
            else
                health_status="FAILED_HEALTH"
                echo "  The canary was NOT promoted. Production is unchanged." >&2
            fi
        elif [ "$rc" -eq 0 ]; then
            echo "  The preview deploy printed no URL, so it cannot be health-checked and is NOT promoted." >&2
            rc=4
        fi
    else
        # Stream provider output live while keeping a copy to parse the URL from.
        # The copy lives in a private mktemp -d dir removed on exit and on INT/TERM/HUP.
        local dep_dir dep_tmp dep_errexit=0
        dep_dir="$(mktemp -d "${TMPDIR:-/tmp}/loki-deploy-out.XXXXXXXX")" || dep_dir=""
        dep_tmp=/dev/null
        if [ -n "$dep_dir" ]; then
            dep_tmp="${dep_dir}/out"
            # shellcheck disable=SC2064
            trap "rm -rf -- '${dep_dir}'; trap - INT TERM HUP; exit 130" INT TERM HUP
        fi
        case $- in *e*) dep_errexit=1 ;; esac
        set +e
        ( cd "$dir" && "${deploy_argv[@]}" ) 2>&1 | tee "$dep_tmp"
        rc=${PIPESTATUS[0]}
        [ "$dep_errexit" -eq 1 ] && set -e
        if [ "$rc" -eq 0 ] && [ "$dep_tmp" != /dev/null ]; then
            # Same last-https-URL parse as the canary path; never invented.
            deployed_url="$(grep -Eo 'https?://[^[:space:]]+' "$dep_tmp" | tail -n 1)" || deployed_url=""
            if [ -n "$deployed_url" ]; then
                echo "Live at: ${deployed_url}"
                if [ -z "$health_url" ]; then health_url="$deployed_url"; health_src="captured"; fi
            fi
        fi
        if [ -n "$dep_dir" ]; then rm -rf -- "$dep_dir"; trap - INT TERM HUP; fi
    fi

    # Health probe only after a deploy that exited 0. Without a URL, or after a
    # failed deploy, status is "unchecked": never a claim the service is healthy.
    if [ "$canary_kind" != vercel ] && [ "$rc" -eq 0 ] && [ -n "$health_url" ]; then
        echo "Probing health: ${health_url}"
        if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
            health_status="healthy"
        else
            health_status="FAILED_HEALTH"
        fi
    fi

    # Live binding: the deployed URL must serve the provenance doc for THIS anchor.
    if [ "$prov_written" = true ] && [ "$rc" -eq 0 ] && [ "$health_status" != "FAILED_HEALTH" ] && [ -n "$deployed_url" ]; then
        echo "Probing live binding: ${deployed_url%/}/.well-known/loki-provenance.json"
        local lb_rc=0
        _deploy_probe_live_binding "$dir" "$deployed_url" "$anchor" || lb_rc=$?
        case "$lb_rc" in
            0)
                live_binding="VERIFIED"
                echo -e "${GREEN}Live binding VERIFIED:${NC} ${deployed_url} serves a signed provenance doc for ${anchor:0:12}."
                ;;
            1)
                live_binding="FAILED"
                health_status="FAILED_HEALTH"
                echo -e "${RED}FAILED_HEALTH: ${deployed_url} served a provenance doc that is forged or is not for ${anchor:0:12} (stale or wrong build served).${NC}" >&2
                ;;
            *)
                echo "  Live binding unchecked: no verifiable provenance doc could be fetched from ${deployed_url}. Not a claim the live build is ${anchor:0:12}." >&2
                ;;
        esac
    fi

    if [ "$health_status" = "FAILED_HEALTH" ] && [ "$live_binding" != "FAILED" ]; then
        echo -e "${RED}FAILED_HEALTH: ${health_url} did not return 2xx${health_match:+ containing \"${health_match}\"} (last status ${hcode:-000}).${NC}" >&2
    fi
    if [ "$health_status" = "FAILED_HEALTH" ]; then
        if [ "$auto_rollback" = true ] && [ -n "$rb_cmd" ] && ! _deploy_is_destructive "$rb_cmd"; then
            echo "Rolling back: ${rb_cmd}" >&2
            rb_attempted=true
            rb_exit=0
            ( cd "$dir" && eval "$rb_cmd" ) || rb_exit=$?
            if [ "$rb_exit" -eq 0 ]; then
                echo "Rollback command exited 0. Re-checking health: ${health_url}" >&2
                if rb_hcode="$(_deploy_probe_health "$health_url")"; then
                    post_rb="healthy"
                    echo -e "${GREEN}Rolled-back service is healthy${NC} (HTTP ${rb_hcode})." >&2
                else
                    post_rb="FAILED_HEALTH"
                    echo -e "${RED}Rolled-back service is STILL unhealthy (last status ${rb_hcode:-000}).${NC}" >&2
                fi
            else
                echo -e "${RED}Rollback command exited ${rb_exit}.${NC}" >&2
            fi
        elif [ "$auto_rollback" = true ]; then
            echo "  No automatic rollback ran: ${rb_note:-no allowlisted rollback command}." >&2
        elif [ -n "$rb_cmd" ]; then
            echo "  Re-run with --auto-rollback to roll back automatically, or run: ${rb_cmd}" >&2
        fi
    fi

    # --domain: only after a deploy that exited 0 and did not fail its health probe.
    local domain="${_LOKI_DEPLOY_DOMAIN:-}" dom_rc=0
    _DD_STATUS="" _DD_TLS="unchecked" _DD_NOTAFTER="" _DD_PROV=""
    if [ -n "$domain" ] && [ "$rc" -eq 0 ] && [ "$health_status" != "FAILED_HEALTH" ]; then
        _deploy_domain_step "$dir" "$command" "$domain" "$deployed_url" "$anchor" || dom_rc=$?
        [ "$_DD_STATUS" != attached ] || exec_disp="${exec_disp}; domain ${domain}"
    fi

    local rec rsha
    finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    rsha="$(python3 -c 'import json,sys;print((json.load(open(sys.argv[1])).get("verification") or {}).get("hash") or "")' "$pj" 2>/dev/null || printf '')"
    rec="$(_deploy_record "$dir" "$exec_disp" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
        "$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit" \
        "$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)" "$dest" "$deployed_url" "$health_src" "$env_keys" "$env_file" \
        "$domain" "$_DD_STATUS" "$_DD_TLS" "$_DD_NOTAFTER" "$_DD_PROV" "$live_binding")"
    echo ""
    if [ "$rc" -eq 0 ]; then
        echo -e "${GREEN}Deploy command exited 0.${NC}"
    else
        echo -e "${RED}Deploy command exited ${rc}.${NC}" >&2
    fi
    if [ "$health_status" = "healthy" ]; then
        echo -e "${GREEN}Health check passed${NC} (HTTP ${hcode})."
    elif [ "$health_status" = "unchecked" ]; then
        echo "Health: unchecked (no --health-url, or the deploy did not exit 0). Not a claim the service is healthy."
    fi
    if [ -n "$rec" ]; then
        echo "Deploy record: ${rec}"
    else
        # The deploy ALREADY RAN. Failing to record it silently is the exact
        # thing this feature exists to prevent -- an executed deploy with no
        # evidence of itself -- so it is reported LOUDLY rather than swallowed.
        # Not an error exit: the deploy's own exit code is the honest verdict on
        # the deploy, and overriding it would misreport what happened.
        echo -e "${RED}WARNING: the deploy ran but its record could NOT be written.${NC}" >&2
        echo "  Command: ${command}" >&2
        echo "  Authorizing receipt run_id: ${run_id:-unknown}; anchor: ${anchor:-unknown}" >&2
        echo "  Record this yourself: a deploy with no record of itself is unevidenced." >&2
    fi
    # A deploy that exited 0 but failed its health probe is a failed deploy.
    if [ "$rc" -eq 0 ] && [ "$health_status" = "FAILED_HEALTH" ]; then
        # 5: the rollback ran and the service is still down.
        [ "$post_rb" = "FAILED_HEALTH" ] && return 5
        return 4
    fi
    # 6: the domain could not be attached or its TLS is invalid.
    if [ "$rc" -eq 0 ] && [ "$dom_rc" -ne 0 ]; then return 6; fi
    return "$rc"
}

# Emitted by _deploy_gate (not a refusal reason) when the anchor was UNPROVEN
# and the operator overrode it. The gate runs inside $( ), so this travels on
# stdout; a global assigned in the subshell would not survive.
_DEPLOY_ANCHOR_MARKER="__LOKI_ANCHOR_UNPROVEN__"

# _deploy_gate <dir> <command>
# THE GATE. Echoes "" when all four checks pass, else the REFUSE reason(s), one
# per line. Every check is evaluated so a refusal names everything wrong at once
# rather than making the user re-run to discover the next problem.
# The opt-in (check 4) is the CALLER's --execute flag and is not re-checked here.
_deploy_gate() {
    local dir="${1:-.}" command="${2:-}"
    local reasons=""

    # Hard limit, checked FIRST: no receipt authorizes a destructive operation.
    if _deploy_is_destructive "$command"; then
        reasons="${reasons}destructive operation refused: no receipt authorizes delete/destroy/teardown/drop
"
    fi

    # An UNFILLED <placeholder> is a command written for a HUMAN to complete,
    # not one to run. Executing it is at best a shell syntax error and at worst
    # an unintended redirection ("--dir=<build-output>" redirects stdin from a
    # file named build-output). Guessing the value would be worse still: we
    # would deploy a directory nobody chose.
    case "$command" in
        *"<"*">"*) reasons="${reasons}deploy command contains an unfilled placeholder -- run it yourself with the real value: ${command}
" ;;
    esac

    local pj
    pj="$(_deploy_latest_receipt "$dir")"
    if [ -z "$pj" ]; then
        reasons="${reasons}no Evidence Receipt found (nothing has verified this tree)
"
    else
        local verdict
        verdict="$(_deploy_receipt_verdict "$pj")"
        case "$verdict" in
            VERIFIED) : ;;
            UNSIGNED)  reasons="${reasons}receipt is UNSIGNED: integrity holds but provenance is unproven (docs/SIGNED-RECEIPTS.md)
" ;;
            UNCHECKED) reasons="${reasons}receipt is UNCHECKED: its signature could not be evaluated here, so nothing was proven
" ;;
            TAMPERED)  reasons="${reasons}receipt is TAMPERED: its integrity hash does not match its contents
" ;;
            *)         reasons="${reasons}receipt verdict could not be determined
" ;;
        esac
        local anchor_reason
        anchor_reason="$(_deploy_anchor_ok "$pj" "$dir")"
        case "$anchor_reason" in
            UNANCHORED:*)
                # Nothing was PROVEN either way. Overridable per invocation, and
                # only the UNANCHORED-prefixed line is dropped: discarding the
                # whole anchor check (or skipping the call) would also swallow
                # the stale-receipt refusal, silently turning this flag into
                # "skip anchoring entirely".
                if [ "${LOKI_DEPLOY_ALLOW_UNANCHORED:-false}" = "true" ]; then
                    # Emitted on STDOUT as a non-reason marker, not stored in a
                    # variable: _deploy_gate is called inside $( ), so a global
                    # assigned here dies with the subshell. The caller strips
                    # this line before treating what remains as refusal reasons.
                    reasons="${reasons}${_DEPLOY_ANCHOR_MARKER}
"
                else
                    reasons="${reasons}${anchor_reason#UNANCHORED:} (re-run with --allow-unanchored to deploy anyway; nothing about the tree will have been proven)
"
                fi
                ;;
            "") : ;;
            *)  # Positive evidence of the wrong build. No flag bypasses this.
                reasons="${reasons}${anchor_reason}
" ;;
        esac
    fi

    if [ "${_LOKI_DEPLOY_DESTINATION:-}" = production ]; then
        _deploy_staging_receipt_ok "$dir" "$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)" \
            || reasons="${reasons}no VERIFIED staging deploy receipt for this SHA (deploy with --destination staging first)
"
    fi

    local dirty
    dirty="$(_deploy_tree_clean "$dir")"
    [ -n "$dirty" ] && reasons="${reasons}${dirty}
"

    local pol
    pol="$(_deploy_policy_refusal "$dir")"
    [ -n "$pol" ] && reasons="${reasons}${pol}
"

    printf '%s' "$reasons"
    return 0
}

# cmd_deploy [--dir <path>] [--no-clip] [--execute] [--help]
# Advisory orchestration: detect project type + CI/CD pipeline + installed cloud
# CLIs, then PRINT the canonical deploy command(s). PRINT-ONLY BY DEFAULT.
# Placed LAST in the contiguous deploy block (all helpers above it) so the SDET
# can extract from the first helper def to the close of cmd_deploy and capture the
# whole self-contained unit (mirrors test-preview-public.sh, where cmd_preview is
# the terminal function).
cmd_deploy() {
    local dir="${TARGET_DIR:-.}"
    local do_clip=true
    local do_execute=false
    # `local` so an AMBIENT LOKI_DEPLOY_ALLOW_UNANCHORED=true in the environment
    # cannot opt in: it is reset to false on every invocation and only the flag
    # below sets it. Same rule --execute already follows -- no env var or config
    # file alone weakens this gate.
    local LOKI_DEPLOY_ALLOW_UNANCHORED=false
    # Per-invocation like --execute; local so an ambient env var cannot set them.
    local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_HEALTH_MATCH="" _LOKI_DEPLOY_AUTO_ROLLBACK=false _LOKI_DEPLOY_CANARY=false _LOKI_DEPLOY_DESTINATION=""
    local _LOKI_DEPLOY_ENV_FILE="" _LOKI_DEPLOY_DOMAIN=""

    # Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
    while [ $# -gt 0 ]; do
        case "${1:-}" in
            --help|-h|help)
                echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
                echo ""
                echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary] [--env-file FILE] [--domain HOST]] [--destination preview|staging|production] [--health-url URL [--health-match STR] [--auto-rollback]]"
                echo ""
                echo "Detects your project type and your installed cloud CLI (and any"
                echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
                echo "By default this is print-only and advisory: it NEVER deploys and"
                echo "NEVER runs a cloud CLI (not even --version). It NEVER runs"
                echo "'git push' on ANY path. Detection is read-only. You run the"
                echo "printed command."
                echo ""
                echo "If a CI/CD pipeline (GitHub Actions, GitLab CI, Jenkins, CircleCI,"
                echo "Azure Pipelines, Bitbucket) is detected, the primary advice is the"
                echo "git push + pull-request path, because your pipeline deploys on merge."
                echo ""
                echo -e "${BOLD}--execute: a deploy authorized by a verified receipt${NC}"
                echo ""
                echo "With --execute, Loki runs the detected deploy command ONLY when ALL"
                echo "of these hold. Any one failing REFUSES, and the refusal names which:"
                echo "  1. An Evidence Receipt exists for this tree and its integrity hash"
                echo "     matches (hash_ok)."
                echo "  2. Its verdict is VERIFIED. UNSIGNED, UNCHECKED and TAMPERED all"
                echo "     refuse -- integrity is not provenance."
                echo "  3. The receipt anchors to the commit being deployed. A receipt for"
                echo "     a different commit does not authorize this deploy."
                echo "     Two outcomes are distinguished here, and only one is overridable:"
                echo "       UNANCHORED  the receipt records no baseline, so neither a match"
                echo "                   nor a mismatch can be proven. Refuses by default;"
                echo "                   --allow-unanchored proceeds and records that the"
                echo "                   anchor was never proven."
                echo "       MISMATCH    a baseline IS recorded and resolves to a different"
                echo "                   tree. That is evidence of the wrong build, and"
                echo "                   --allow-unanchored does NOT bypass it."
                echo "  4. You passed --execute on THIS invocation. No env var or config"
                echo "     file alone can opt in."
                echo "  5. Policy permits it. If .loki/policies.json (or the global one) exists,"
                echo "     deploy needs autonomy 'operate' or actions.deploy 'allow'. An"
                echo "     approval-required, denied, unevaluable or permission-less (neither key set) policy REFUSES. No policy"
                echo "     file leaves the gate unchanged."
                echo ""
                echo "It FAILS CLOSED: if a check cannot be evaluated at all (no receipt,"
                echo "signing key or keys unavailable, anchor unresolvable, not a git repo), that is a"
                echo "REFUSAL, not a pass. A refusal still prints the advisory command."
                echo ""
                echo "Hard limits, with or without --execute:"
                echo "  - NEVER runs a destructive cloud operation (delete, destroy,"
                echo "    teardown, drop). Refused even with a valid receipt."
                echo "  - NEVER runs 'git push' and NEVER creates a PR. Out of scope."
                echo "  - NEVER reads or forwards cloud credentials. The deploy command"
                echo "    inherits your already-authenticated CLI session."
                echo "  - REFUSES if the working tree is dirty: deploying something other"
                echo "    than what was verified is the exact failure this prevents."
                echo ""
                echo "Every executed deploy writes its own record to .loki/deploys/:"
                echo "the command, its exit code, the authorizing receipt's run_id, the"
                echo "resolved anchor, and anchor_state -- so a reader can tell later"
                echo "whether the anchor was proven or explicitly overridden."
                echo ""
                echo "Health check and rollback (only with --execute):"
                echo "  --health-url URL  After a deploy that exits 0, poll URL (bounded). Any"
                echo "                    non-2xx marks the deploy FAILED_HEALTH and exits 4."
                echo "                    Without a URL, health is recorded as 'unchecked',"
                echo "                    never as healthy."
                echo "  --health-match S  The 2xx body must also contain S (for example the HEAD sha"
                echo "                    or version), else FAILED_HEALTH. Needs --health-url."
                echo "  --auto-rollback   On FAILED_HEALTH run the provider's documented rollback:"
                echo "                    'vercel rollback' or 'fly releases rollback <version"
                echo "                    captured before the deploy>'. Netlify and Cloudflare"
                echo "                    print a manual note. Needs --health-url. The rolled-back"
                echo "                    service is re-probed and recorded as post_rollback_health;"
                echo "                    still unhealthy exits 5."
                echo ""
                echo "Staged rollout (only with --execute):"
                echo "  --canary          Vercel: deploy without --prod, health-check the preview"
                echo "                    URL, then run 'vercel promote <url>'; a failed probe never"
                echo "                    promotes. Fly: 'fly deploy --strategy canary'. Other"
                echo "                    providers document no staged path and refuse --canary."
                echo ""
                echo "The executed deploy's record in .loki/deploys/ is a signed loki.deploy/1"
                echo "attestation bound to the receipt (receipt_sha256). Re-check it offline with"
                echo "'loki verify <record.json>' (add --pubkey FILE to use a given key). It fails"
                echo "closed: a tampered, unsigned or unkeyed record, or a cited receipt that is not"
                echo "VERIFIED, is never reported as VERIFIED."
                echo ""
                echo "Options:"
                echo "  --dir <path>   Project directory to scan (default: current dir)"
                echo "  --no-clip      Do not copy the idiomatic command to the clipboard"
                echo "  --execute      Run the deploy command if the receipt gate passes"
                echo "  --destination preview|staging|production"
                echo "                 With --execute: preview/staging run a non-production deploy"
                echo "                 (Vercel only); production is refused unless a VERIFIED staging"
                echo "                 deploy record exists for the same SHA. Unset keeps today's behavior."
                echo "  --env-file FILE"
                echo "                 With --execute: push KEY=VALUE lines from FILE to the provider"
                echo "                 (parsed, never shell-evaluated). Refuses when a key listed in"
                echo "                 .env.example is missing from FILE. Vercel: 'vercel --prod -e K=V'"
                echo "                 per key; Fly: 'fly secrets set --stage K=V ...' then 'fly deploy';"
                echo "                 Netlify: 'netlify env:import FILE'; Cloudflare: 'wrangler secret"
                echo "                 bulk FILE'. Any other provider refuses with nothing run. If the env"
                echo "                 step fails the deploy is not run. Values are masked as K=*** in"
                echo "                 output; the deploy record stores env_keys (names) and env_source,"
                echo "                 never values."
                echo "  --domain HOST  With --execute, after a deploy that exited 0 and is not FAILED_HEALTH:"
                echo "                 attach HOST (Vercel: 'vercel alias set <deployed_url> HOST'; Fly:"
                echo "                 'fly certs add HOST'; Netlify and Cloudflare print the manual dashboard"
                echo "                 step, domain_status=manual). Then prove TLS with openssl s_client and"
                echo "                 'openssl x509 -checkend 604800' and re-probe"
                echo "                 https://HOST/.well-known/loki-provenance.json. The record stores domain,"
                echo "                 domain_status, tls_status (valid|expiring|invalid|unchecked) and"
                echo "                 tls_not_after; no openssl means unchecked, never valid. An invalid TLS"
                echo "                 handshake or a failed attach exits 6 and prints the DNS record needed."
                echo "  --allow-unanchored"
                echo "                 Proceed when the receipt records NO baseline (nothing"
                echo "                 about this tree is proven). Never bypasses a recorded"
                echo "                 mismatch. Per-invocation only; no env var opts in."
                echo "  --help, -h     Show this help and exit"
                return 0
                ;;
            --execute)
                do_execute=true
                ;;
            --allow-unanchored)
                # Per-invocation, like --execute: exported for the gate to read,
                # never persisted and never settable by config alone.
                LOKI_DEPLOY_ALLOW_UNANCHORED=true
                ;;
            --health-url)
                case "${2:-}" in
                    http://*|https://*) _LOKI_DEPLOY_HEALTH_URL="$2"; shift ;;
                    *) echo "loki deploy --health-url requires an http(s) URL" >&2; return 1 ;;
                esac
                ;;
            --health-match)
                [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo "loki deploy --health-match requires a string" >&2; return 1; }
                _LOKI_DEPLOY_HEALTH_MATCH="$2"; shift
                ;;
            --auto-rollback)
                _LOKI_DEPLOY_AUTO_ROLLBACK=true
                ;;
            --canary)
                _LOKI_DEPLOY_CANARY=true
                ;;
            --destination)
                case "${2:-}" in
                    preview|staging|production) _LOKI_DEPLOY_DESTINATION="$2"; shift ;;
                    *) echo "loki deploy --destination must be one of: preview, staging, production" >&2; return 1 ;;
                esac
                ;;
            --dir)
                # Guard against a missing OR flag-shaped value: an unguarded
                # shift when --dir is the last arg underflows under set -e, and a
                # bare "--dir --no-clip" would otherwise treat the next flag as
                # the scan directory.
                case "${2:-}" in
                    -*|"") echo "loki deploy --dir requires a directory path" >&2; return 1 ;;
                    *) dir="$2"; shift ;;
                esac
                ;;
            --no-clip)
                do_clip=false
                ;;
            --env-file)
                case "${2:-}" in
                    -*|"") echo "loki deploy --env-file requires a file path" >&2; return 1 ;;
                    *) _LOKI_DEPLOY_ENV_FILE="$2"; shift ;;
                esac
                ;;
            --env-file=*)
                _LOKI_DEPLOY_ENV_FILE="${1#--env-file=}"
                [ -n "$_LOKI_DEPLOY_ENV_FILE" ] || { echo "loki deploy --env-file requires a file path" >&2; return 1; }
                ;;
            --domain)
                case "${2:-}" in
                    -*|"") echo "loki deploy --domain requires a hostname" >&2; return 1 ;;
                    *) _LOKI_DEPLOY_DOMAIN="$2"; shift ;;
                esac
                ;;
            --domain=*)
                _LOKI_DEPLOY_DOMAIN="${1#--domain=}"
                [ -n "$_LOKI_DEPLOY_DOMAIN" ] || { echo "loki deploy --domain requires a hostname" >&2; return 1; }
                ;;
        esac
        shift
    done

    if [ -n "$_LOKI_DEPLOY_ENV_FILE" ]; then
        if [ "$do_execute" != true ]; then
            echo "loki deploy --env-file requires --execute (the advisory run pushes nothing)" >&2
            return 1
        fi
        if [ ! -f "$_LOKI_DEPLOY_ENV_FILE" ] || [ ! -r "$_LOKI_DEPLOY_ENV_FILE" ]; then
            echo "loki deploy --env-file: not a readable file: $_LOKI_DEPLOY_ENV_FILE" >&2
            return 1
        fi
        # Absolute, so a provider verb run from the project dir finds the same file.
        _LOKI_DEPLOY_ENV_FILE="$(cd "$(dirname -- "$_LOKI_DEPLOY_ENV_FILE")" && pwd -P)/$(basename -- "$_LOKI_DEPLOY_ENV_FILE")"
    fi

    if [ -n "$_LOKI_DEPLOY_DOMAIN" ]; then
        if [ "$do_execute" != true ]; then
            echo "loki deploy --domain requires --execute (the advisory run attaches nothing)" >&2
            return 1
        fi
        _LOKI_DEPLOY_DOMAIN="$(printf '%s' "$_LOKI_DEPLOY_DOMAIN" | tr '[:upper:]' '[:lower:]')"
        if ! printf '%s' "$_LOKI_DEPLOY_DOMAIN" | grep -Eq '^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$'; then
            echo "loki deploy --domain: not a valid hostname: $_LOKI_DEPLOY_DOMAIN" >&2
            return 1
        fi
    fi

    if [ "$_LOKI_DEPLOY_AUTO_ROLLBACK" = true ] && [ -z "$_LOKI_DEPLOY_HEALTH_URL" ]; then
        echo "loki deploy --auto-rollback requires --health-url (no health signal, no rollback)" >&2
        return 1
    fi
    if [ -n "$_LOKI_DEPLOY_HEALTH_MATCH" ] && [ -z "$_LOKI_DEPLOY_HEALTH_URL" ]; then
        echo "loki deploy --health-match requires --health-url" >&2
        return 1
    fi
    if [ "$_LOKI_DEPLOY_CANARY" = true ] && [ "$do_execute" != true ]; then
        echo "loki deploy --canary requires --execute" >&2
        return 1
    fi
    if [ "$_LOKI_DEPLOY_CANARY" = true ] && [ -n "$_LOKI_DEPLOY_DESTINATION" ] && [ "$_LOKI_DEPLOY_DESTINATION" != production ]; then
        echo "loki deploy --canary only applies to --destination production" >&2
        return 1
    fi
    if [ "$do_execute" != true ] && [ -n "$_LOKI_DEPLOY_HEALTH_URL" ]; then
        echo "Note: --health-url and --auto-rollback only apply with --execute; ignored on this advisory run." >&2
    fi

    # Resolve to '.' if the chosen dir does not exist (honest, no crash).
    [ -d "$dir" ] || dir="."

    # Tells the advisory printer which closing sentence is TRUE for this run.
    # Scoped to this invocation only.
    local _LOKI_DEPLOY_EXECUTING="$do_execute"

    # Detect CI/CD pipeline FIRST (BRANCH-LIFECYCLE LOCK B3 precedence).
    local cicd=""
    cicd="$(_deploy_detect_cicd "$dir" || true)"
    local has_pipeline=false
    [ -n "$cicd" ] && has_pipeline=true

    # Detect project type (filesystem-only; LOCK 6 -- not gated on a running app).
    local type=""
    type="$(_deploy_detect_type "$dir")"

    # ---- Pipeline path (LOCK B3): git push + PR advised FIRST, cloud secondary.
    if [ "$has_pipeline" = true ]; then
        echo -e "${BOLD}CI/CD pipeline detected (${cicd})${NC}"
        echo "Your pipeline deploys on push/merge, so the primary deploy path is"
        echo "commit + push + pull request:"
        echo ""

        # Derive base/head for the PR advice. set -e safe: every git call is
        # 2>/dev/null with a fallback so a non-zero git never aborts the shell.
        local head="" base=""
        head="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
        [ -n "$head" ] || head="HEAD"
        case "$head" in
            loki/*)
                # On a loki branch: prefer the persisted base, else origin default.
                if [ -s "$dir/.loki/state/base-branch.txt" ]; then
                    base="$(head -n1 "$dir/.loki/state/base-branch.txt" 2>/dev/null || echo "")"
                fi
                ;;
        esac
        if [ -z "$base" ]; then
            # Best-effort: origin's default branch (e.g. origin/main -> main).
            local origin_head=""
            origin_head="$(git -C "$dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")"
            if [ -n "$origin_head" ]; then
                base="${origin_head##*/}"
            fi
        fi

        if [ -n "$base" ] && declare -F print_pr_advice >/dev/null 2>&1; then
            print_pr_advice "$base" "$head" "$dir"
        elif declare -F print_pr_advice >/dev/null 2>&1; then
            echo "  Could not determine the PR base branch automatically."
            echo "  Set your PR base manually when opening the pull request for: ${head}"
            print_pr_advice "manually-set-base" "$head" "$dir"
        else
            echo "  git push -u origin ${head}"
            echo "  Open a pull request for ${head} (set the base branch manually)."
        fi
        echo ""

        # Cloud CLI options are SECONDARY when a pipeline exists. Print them if a
        # project type + installed CLI match, but never fail the command on their
        # absence (the git/PR advice above is the real deliverable here).
        if [ -n "$type" ]; then
            # Pass do_clip=false here: in the pipeline path the git push line is
            # the PRIMARY advice (LOCK B3) and print_pr_advice already copied it to
            # the clipboard, so the secondary cloud command must NOT overwrite it.
            local printed_any=false
            _deploy_print_cloud_options "$dir" "$type" "false" "false" && printed_any=true || true
            if [ "$printed_any" != true ]; then
                : # No installed cloud CLI; the pipeline path already advised. Fine.
            fi
        fi
        # --execute applies to the CLOUD command only. git push / PR creation
        # stay out of scope entirely, so on the pipeline path --execute never
        # touches the primary (git) advice above -- it can only run the
        # secondary cloud command, and only if the receipt gate passes.
        if [ "$do_execute" = true ]; then
            if [ -n "$type" ]; then
                _deploy_try_execute "$dir" "$type"
                return $?
            fi
            echo "" >&2
            echo -e "${RED}REFUSED: --execute has nothing to run here.${NC}" >&2
            echo "  No deployable project type detected, and Loki never runs 'git push'" >&2
            echo "  or creates a PR. Run the git advice above yourself." >&2
            return 1
        fi
        return 0
    fi

    # ---- No-pipeline path: cloud-CLI advisory exactly per DEPLOY-PLAN.md.
    # Honest failure: no project type detected.
    if [ -z "$type" ]; then
        {
            echo "No deployable project detected in: ${dir}"
            echo "Looked for: package.json (Next.js/Vite/CRA/Node), Dockerfile,"
            echo "dist/ or build/ with index.html, requirements.txt, pyproject.toml."
            echo "Run 'loki deploy --dir <path>' to point at your project directory."
        } >&2
        return 1
    fi

    # Print cloud options; clipboard the idiomatic one. Returns non-zero (with an
    # honest install hint) when NO matching CLI is installed.
    local rc=0
    _deploy_print_cloud_options "$dir" "$type" "$do_clip" "true" || rc=$?

    # Execute ONLY if the user opted in on THIS invocation and the advisory
    # above found something to run. The advisory has already printed, so a
    # refusal below still leaves the user with today's output.
    if [ "$do_execute" = true ] && [ "$rc" -eq 0 ]; then
        _deploy_try_execute "$dir" "$type"
        return $?
    fi
    return $rc
}

# Import GitHub issues
cmd_import() {
    # v7.6.2 B-13 fix: --help must print help, not start an import.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- import GitHub issues into the queue${NC}"
            echo ""
            echo "Usage: loki import [options]"
            echo ""
            echo "Imports issues from the current repo's GitHub project into"
            echo ".loki/queue/ so loki start picks them up as tasks."
            echo ""
            echo "Options:"
            echo "  --help, -h   Show this help and exit"
            echo ""
            echo "Side effects: makes GitHub API calls, writes to .loki/queue/."
            return 0
            ;;
    esac
    if [ ! -d "$LOKI_DIR" ]; then
        mkdir -p "$LOKI_DIR/queue"
    fi

    export LOKI_GITHUB_IMPORT=true

    # Check gh CLI
    if ! command -v gh &> /dev/null; then
        echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}"
        exit 1
    fi

    if ! gh auth status &> /dev/null; then
        echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}"
        exit 1
    fi

    echo -e "${GREEN}Importing GitHub issues...${NC}"
    # Source the functions from run.sh and call import
    source "$RUN_SH" 2>/dev/null || true
    if type import_github_issues &>/dev/null; then
        import_github_issues
    else
        echo -e "${YELLOW}Import function not available. Using fallback.${NC}"
        gh issue list --json number,title,url --limit 20
    fi
}

# GitHub integration management (v5.41.0)
# _gh_protect_py: JSON/URL helper for `loki github protect`.
#   enc BRANCH | put GETJSON ADD | has GETJSON | same GETJSON WRITTEN
#   mkstate SLUG BRANCH MODE PRIOR WRITTEN | sget STATEJSON KEY
# `put` turns a GET branch-protection body into the PUT body, carrying every
# setting (reviews, admins, restrictions, linear history, check app_ids)
# unchanged; ADD=1 appends the loki/receipt check.
_gh_protect_py() {
    python3 -c '
import json, sys, urllib.parse
op = sys.argv[1]
def names(lst, key):
    return [x if isinstance(x, str) else x.get(key) for x in (lst or [])]
def actors(d):
    if d is None:
        return None
    return {"users": names(d.get("users"), "login"), "teams": names(d.get("teams"), "slug"), "apps": names(d.get("apps"), "slug")}
def put(p, add):
    rsc = p.get("required_status_checks")
    rs = None
    if rsc is not None or add:
        rsc = rsc or {}
        checks = rsc.get("checks")
        if checks is None:
            checks = [{"context": c} for c in rsc.get("contexts", [])]
        checks = [{k: v for k, v in c.items() if not (k == "app_id" and v is None)} for c in checks]
        if add and not any(c.get("context") == "loki/receipt" for c in checks):
            checks.append({"context": "loki/receipt"})
        rs = {"strict": bool(rsc.get("strict")), "checks": checks}
    rv = p.get("required_pull_request_reviews")
    if rv is not None:
        r = {"dismiss_stale_reviews": bool(rv.get("dismiss_stale_reviews")),
             "require_code_owner_reviews": bool(rv.get("require_code_owner_reviews")),
             "required_approving_review_count": rv.get("required_approving_review_count", 0),
             "require_last_push_approval": bool(rv.get("require_last_push_approval"))}
        for k in ("dismissal_restrictions", "bypass_pull_request_allowances"):
            if rv.get(k) is not None:
                r[k] = actors(rv[k])
        rv = r
    def en(k):
        return bool((p.get(k) or {}).get("enabled"))
    body = {"required_status_checks": rs, "enforce_admins": en("enforce_admins"),
            "required_pull_request_reviews": rv, "restrictions": actors(p.get("restrictions"))}
    for k in ("required_linear_history", "allow_force_pushes", "allow_deletions", "block_creations",
              "required_conversation_resolution", "lock_branch", "allow_fork_syncing"):
        body[k] = en(k)
    return body
def norm(b):
    b = json.loads(json.dumps(b))
    if b.get("required_status_checks"):
        for c in b["required_status_checks"]["checks"]:
            c.pop("app_id", None)
    return b
def dump(o):
    return json.dumps(o, sort_keys=True, separators=(",", ":"))
if op == "enc":
    print(urllib.parse.quote(sys.argv[2], safe="/"))
elif op == "put":
    print(dump(put(json.loads(sys.argv[2]), sys.argv[3] == "1")))
elif op == "has":
    p = json.loads(sys.argv[2]); r = p.get("required_status_checks") or {}
    have = [c.get("context") for c in r.get("checks", [])] + list(r.get("contexts", []))
    print("1" if "loki/receipt" in have else "0")
elif op == "same":
    print("1" if norm(put(json.loads(sys.argv[2]), False)) == norm(json.loads(sys.argv[3])) else "0")
elif op == "mkstate":
    print(dump({"slug": sys.argv[2], "branch": sys.argv[3], "mode": sys.argv[4],
                "prior": json.loads(sys.argv[5]) if sys.argv[5] else None,
                "written": json.loads(sys.argv[6])}))
elif op == "sget":
    v = json.loads(sys.argv[2])[sys.argv[3]]
    print(v if isinstance(v, str) else dump(v))
' "$@"
}

cmd_github() {
    local subcmd="${1:-help}"
    shift 2>/dev/null || true

    case "$subcmd" in
        sync)
            # Sync completed tasks back to GitHub issues
            if [ ! -d "$LOKI_DIR" ]; then
                echo -e "${RED}No active Loki session found${NC}"
                exit 1
            fi

            if ! command -v gh &>/dev/null; then
                echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}"
                exit 1
            fi

            if ! gh auth status &>/dev/null; then
                echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}"
                exit 1
            fi

            export LOKI_GITHUB_SYNC=true
            source "$RUN_SH" 2>/dev/null || true

            echo -e "${GREEN}Syncing completed tasks to GitHub...${NC}"
            if type sync_github_completed_tasks &>/dev/null; then
                sync_github_completed_tasks
                echo -e "${GREEN}Sync complete.${NC}"

                # Show what was synced
                if [ -f "$LOKI_DIR/github/synced.log" ]; then
                    local count
                    count=$(wc -l < "$LOKI_DIR/github/synced.log" | tr -d ' ')
                    echo -e "${DIM}Total synced status updates: $count${NC}"
                fi
            else
                echo -e "${YELLOW}Sync function not available.${NC}"
            fi
            ;;

        export)
            # Export local tasks as GitHub issues
            if [ ! -d "$LOKI_DIR" ]; then
                echo -e "${RED}No active Loki session found${NC}"
                exit 1
            fi

            if ! command -v gh &>/dev/null; then
                echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}"
                exit 1
            fi

            echo -e "${GREEN}Exporting local tasks to GitHub issues...${NC}"
            source "$RUN_SH" 2>/dev/null || true
            if type export_tasks_to_github &>/dev/null; then
                export_tasks_to_github
                echo -e "${GREEN}Export complete.${NC}"
            else
                echo -e "${YELLOW}Export function not available.${NC}"
            fi
            ;;

        pr)
            # Create PR from completed work
            if ! command -v gh &>/dev/null; then
                echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}"
                exit 1
            fi

            local feature_name="${1:-Loki Mode changes}"
            export LOKI_GITHUB_PR=true
            source "$RUN_SH" 2>/dev/null || true

            echo -e "${GREEN}Creating pull request: $feature_name${NC}"
            if type create_github_pr &>/dev/null; then
                create_github_pr "$feature_name"
            else
                echo -e "${YELLOW}PR function not available.${NC}"
            fi
            ;;

        status)
            # Show GitHub integration status
            echo -e "${BOLD}GitHub Integration Status${NC}"
            echo ""

            # gh CLI
            if command -v gh &>/dev/null; then
                echo -e "  gh CLI:       ${GREEN}installed $(gh --version 2>/dev/null | head -1 | sed 's/gh version /v/')${NC}"
                if gh auth status &>/dev/null 2>&1; then
                    echo -e "  Auth:         ${GREEN}authenticated${NC}"
                else
                    echo -e "  Auth:         ${RED}not authenticated${NC}"
                fi
            else
                echo -e "  gh CLI:       ${RED}not installed${NC}"
            fi

            # Repo detection
            local repo=""
            repo=$(git remote get-url origin 2>/dev/null | sed 's|.*github.com[:/]||;s|\.git$||' || echo "")
            if [ -n "$repo" ]; then
                echo -e "  Repository:   ${GREEN}$repo${NC}"
            else
                echo -e "  Repository:   ${YELLOW}not detected${NC}"
            fi

            # Config flags
            echo ""
            echo -e "${BOLD}Configuration${NC}"
            echo -e "  LOKI_GITHUB_IMPORT:  ${LOKI_GITHUB_IMPORT:-false}"
            echo -e "  LOKI_GITHUB_SYNC:    ${LOKI_GITHUB_SYNC:-false}"
            echo -e "  LOKI_GITHUB_PR:      ${LOKI_GITHUB_PR:-false}"
            echo -e "  LOKI_GITHUB_LABELS:  ${LOKI_GITHUB_LABELS:-(all)}"
            echo -e "  LOKI_GITHUB_LIMIT:   ${LOKI_GITHUB_LIMIT:-100}"

            # Sync log
            if [ -f "$LOKI_DIR/github/synced.log" ]; then
                echo ""
                echo -e "${BOLD}Sync History${NC}"
                local total
                total=$(wc -l < "$LOKI_DIR/github/synced.log" | tr -d ' ')
                echo -e "  Total synced updates: $total"
                echo -e "  Recent:"
                tail -5 "$LOKI_DIR/github/synced.log" | sed 's/^/    /'
            fi

            # Imported tasks
            if [ -f "$LOKI_DIR/queue/pending.json" ]; then
                local gh_tasks
                gh_tasks=$(python3 -c "
import json
try:
    with open('$LOKI_DIR/queue/pending.json') as f:
        data = json.load(f)
    tasks = data.get('tasks', data) if isinstance(data, dict) else data
    gh = [t for t in tasks if t.get('source') == 'github']
    print(len(gh))
except: print(0)
" 2>/dev/null || echo "0")
                echo ""
                echo -e "${BOLD}Imported Issues${NC}"
                echo -e "  GitHub tasks in queue: $gh_tasks"
            fi
            ;;

        publish)
            # Receipt-stamped export: push the verified tree to GitHub and set a
            # loki/receipt commit status whose state comes ONLY from the receipt
            # verdict. Never stages files; private unless --public is passed.
            local _pub_vis="--private" _pub_name="" _pub_dir="." _pub_usage=0
            while [ $# -gt 0 ]; do
                case "$1" in
                    --public) _pub_vis="--public"; shift ;;
                    --private) _pub_vis="--private"; shift ;;
                    --dir)
                        case "${2:-}" in ""|-*) _pub_usage=1; break ;; esac
                        _pub_dir="$2"; shift 2 ;;
                    -*) _pub_usage=1; break ;;
                    *)
                        [ -z "$_pub_name" ] || { _pub_usage=1; break; }
                        _pub_name="$1"; shift ;;
                esac
            done
            if [ "$_pub_usage" -ne 0 ]; then
                echo "Usage: loki github publish [name] [--public] [--dir <path>]" >&2
                exit 2
            fi
            local _pub_refuse
            _pub_refuse="$(_deploy_tree_clean "$_pub_dir")"
            if [ -n "$_pub_refuse" ]; then
                echo -e "${RED}Refused: ${_pub_refuse}${NC}" >&2
                exit 1
            fi
            if ! command -v gh &>/dev/null; then
                echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}" >&2
                exit 1
            fi
            if ! gh auth status &>/dev/null; then
                echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}" >&2
                exit 1
            fi
            local _pub_sha _pub_pj _pub_verdict="NONE" _pub_anchor="" _pub_state="none"
            _pub_sha="$(git -C "$_pub_dir" rev-parse HEAD 2>/dev/null)" || _pub_sha=""
            if [ -z "$_pub_sha" ]; then
                echo -e "${RED}Refused: no HEAD commit to publish${NC}" >&2
                exit 1
            fi
            _pub_pj="$(_deploy_latest_receipt "$_pub_dir")"
            if [ -n "$_pub_pj" ]; then
                _pub_verdict="$(_deploy_receipt_verdict "$_pub_pj")"
                _pub_anchor="$(_deploy_anchor_ok "$_pub_pj" "$_pub_dir")"
                if [ "$_pub_verdict" = "VERIFIED" ] && [ -z "$_pub_anchor" ]; then
                    _pub_state="success"
                else
                    _pub_state="failure"
                fi
            fi

            local _pub_url="" _pub_out=""
            if git -C "$_pub_dir" remote get-url origin &>/dev/null; then
                if [ -n "$_pub_anchor" ] || [ -z "$_pub_pj" ]; then
                    echo -e "${RED}Refused: HEAD is not confirmed as the receipt anchor (${_pub_anchor:-no Evidence Receipt found}). Nothing was pushed.${NC}" >&2
                    exit 1
                fi
                git -C "$_pub_dir" push origin HEAD || exit 1
            else
                [ -n "$_pub_name" ] || _pub_name="$(basename "$(cd "$_pub_dir" && pwd)")"
                _pub_out="$(cd "$_pub_dir" && gh repo create "$_pub_name" "$_pub_vis" --source . --push 2>&1)" || {
                    echo "$_pub_out" >&2
                    echo -e "${RED}gh repo create failed; nothing published.${NC}" >&2
                    exit 1
                }
                _pub_url="$(printf '%s\n' "$_pub_out" | grep -Eo 'https://github\.com/[^[:space:]]+' | head -1)"
            fi
            [ -n "$_pub_url" ] || _pub_url="$(git -C "$_pub_dir" remote get-url origin 2>/dev/null || true)"

            local _pub_slug
            _pub_slug="$(printf '%s' "$_pub_url" | sed -E 's|^.*github\.com[:/]||; s|\.git$||')"
            if [ "$_pub_state" = "none" ]; then
                echo -e "${YELLOW}No Evidence Receipt found: no loki/receipt status set.${NC}"
            elif [ -z "$_pub_slug" ] || [ "$_pub_slug" = "$_pub_url" ]; then
                echo -e "${YELLOW}Could not resolve owner/repo: no loki/receipt status set.${NC}"
            else
                gh api "repos/${_pub_slug}/statuses/${_pub_sha}" \
                    -f "state=${_pub_state}" -f "context=loki/receipt" \
                    -f "description=Evidence Receipt verdict: ${_pub_verdict}" >/dev/null \
                    || echo -e "${YELLOW}Warning: could not set the loki/receipt status.${NC}" >&2
                echo "loki/receipt status: ${_pub_state} (receipt ${_pub_verdict})"
            fi
            if [ -n "$_pub_url" ]; then
                echo "Repository: ${_pub_url}"
            else
                echo "Repository URL could not be determined."
            fi
            ;;

        protect)
            # Make loki/receipt a required status check on the default branch.
            # The FULL prior protection is saved (with slug and branch) and the
            # PUT carries every other setting back unchanged, so --off restores
            # the exact prior protection. --dry-run performs one read only.
            local _pr_dry=0 _pr_off=0 _pr_usage=0
            while [ $# -gt 0 ]; do
                case "$1" in
                    --dry-run) _pr_dry=1; shift ;;
                    --off) _pr_off=1; shift ;;
                    *) _pr_usage=1; break ;;
                esac
            done
            if [ "$_pr_usage" -ne 0 ]; then
                echo "Usage: loki github protect [--dry-run] [--off]" >&2
                exit 2
            fi
            if ! command -v gh &>/dev/null; then
                echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}" >&2
                exit 1
            fi
            if ! gh auth status &>/dev/null; then
                echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}" >&2
                exit 1
            fi
            local _pr_url _pr_slug _pr_branch _pr_enc
            _pr_url="$(git remote get-url origin 2>/dev/null || true)"
            _pr_slug="$(printf '%s' "$_pr_url" | sed -E 's|^.*github\.com[:/]||; s|\.git$||')"
            if [ -z "$_pr_slug" ] || [ "$_pr_slug" = "$_pr_url" ]; then
                echo -e "${RED}Error: no GitHub origin remote to protect.${NC}" >&2
                exit 1
            fi
            _pr_branch="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' || true)"
            [ -n "$_pr_branch" ] || _pr_branch="$(git branch --show-current 2>/dev/null)"
            if [ -z "$_pr_branch" ]; then
                echo -e "${RED}Error: could not determine the default branch.${NC}" >&2
                exit 1
            fi
            _pr_enc="$(_gh_protect_py enc "$_pr_branch")" || exit 1
            local _pr_api="repos/${_pr_slug}/branches/${_pr_enc}/protection"
            local _pr_state="${LOKI_DIR}/state/github-protect.json"

            # Read the full current protection: mode is "none" (unprotected)
            # or "protected" (cur holds the JSON).
            local _pr_out _pr_rc=0 _pr_mode="protected" _pr_cur=""
            _pr_out="$(gh api "$_pr_api" 2>&1)" || _pr_rc=$?
            if [ "$_pr_rc" -eq 0 ]; then
                _pr_cur="$_pr_out"
            elif printf '%s' "$_pr_out" | grep -qi "Branch not protected"; then
                _pr_mode="none"
            else
                echo "$_pr_out" >&2
                echo -e "${RED}Error: could not read branch protection for ${_pr_slug}@${_pr_branch}.${NC}" >&2
                exit 1
            fi

            if [ "$_pr_off" -eq 1 ]; then
                if [ ! -f "$_pr_state" ]; then
                    echo -e "${RED}Error: nothing to restore (no saved state at ${_pr_state}).${NC}" >&2
                    exit 1
                fi
                local _pr_st _pr_ssl _pr_sbr _pr_smode _pr_prior _pr_written _pr_body
                _pr_st="$(cat "$_pr_state")"
                _pr_ssl="$(_gh_protect_py sget "$_pr_st" slug)" || { echo -e "${RED}Error: unreadable saved state ${_pr_state}.${NC}" >&2; exit 1; }
                _pr_sbr="$(_gh_protect_py sget "$_pr_st" branch)" || exit 1
                _pr_smode="$(_gh_protect_py sget "$_pr_st" mode)" || exit 1
                if [ "$_pr_ssl" != "$_pr_slug" ] || [ "$_pr_sbr" != "$_pr_branch" ]; then
                    echo -e "${RED}Refused: saved state is for ${_pr_ssl}@${_pr_sbr}, but this checkout is ${_pr_slug}@${_pr_branch}. Nothing changed.${NC}" >&2
                    exit 1
                fi
                if [ "$_pr_mode" = "none" ]; then
                    echo -e "${RED}Refused: ${_pr_slug}@${_pr_branch} has no protection now, so loki/receipt is gone. Nothing changed.${NC}" >&2
                    exit 1
                fi
                _pr_written="$(_gh_protect_py sget "$_pr_st" written)" || exit 1
                if [ "$(_gh_protect_py has "$_pr_cur")" != "1" ]; then
                    echo -e "${RED}Refused: loki/receipt is no longer a required check on ${_pr_slug}@${_pr_branch}; protection was changed since protect. Nothing changed.${NC}" >&2
                    exit 1
                fi
                if [ "$(_gh_protect_py same "$_pr_cur" "$_pr_written")" != "1" ]; then
                    echo -e "${RED}Refused: protection on ${_pr_slug}@${_pr_branch} differs from what protect wrote; restoring would overwrite later changes. Nothing changed.${NC}" >&2
                    exit 1
                fi
                if [ "$_pr_smode" = "none" ]; then
                    gh api -X DELETE "$_pr_api" >/dev/null || { echo -e "${RED}Error: could not remove branch protection.${NC}" >&2; exit 1; }
                else
                    _pr_prior="$(_gh_protect_py sget "$_pr_st" prior)" || exit 1
                    _pr_body="$(_gh_protect_py put "$_pr_prior" 0)" || exit 1
                    printf '%s' "$_pr_body" | gh api -X PUT "$_pr_api" --input - >/dev/null \
                        || { echo -e "${RED}Error: could not restore branch protection.${NC}" >&2; exit 1; }
                fi
                rm -f "$_pr_state"
                echo "Restored branch protection on ${_pr_slug}@${_pr_branch} (was: ${_pr_smode})"
                exit 0
            fi

            local _pr_has=0
            if [ "$_pr_mode" = "protected" ] && [ "$(_gh_protect_py has "$_pr_cur")" = "1" ]; then
                _pr_has=1
            fi
            if [ "$_pr_dry" -eq 1 ]; then
                echo "Dry run for ${_pr_slug}@${_pr_branch}: protection is '${_pr_mode}'; would $([ "$_pr_has" -eq 1 ] && echo 'change nothing (loki/receipt already required)' || echo 'require loki/receipt'). Nothing changed."
                exit 0
            fi
            if [ "$_pr_has" -eq 1 ]; then
                echo "loki/receipt is already a required check on ${_pr_slug}@${_pr_branch}; nothing to do."
                exit 0
            fi
            local _pr_new _pr_prior_in="" _pr_src="{}" _pr_stjson
            if [ "$_pr_mode" = "protected" ]; then _pr_prior_in="$_pr_cur"; _pr_src="$_pr_cur"; fi
            _pr_new="$(_gh_protect_py put "$_pr_src" 1)" || exit 1
            _pr_stjson="$(_gh_protect_py mkstate "$_pr_slug" "$_pr_branch" "$_pr_mode" "$_pr_prior_in" "$_pr_new")" || exit 1
            mkdir -p "${LOKI_DIR}/state"
            printf '%s\n' "$_pr_stjson" > "$_pr_state"
            if ! printf '%s' "$_pr_new" | gh api -X PUT "$_pr_api" --input - >/dev/null; then
                rm -f "$_pr_state"
                echo -e "${RED}Error: could not update branch protection; nothing changed.${NC}" >&2
                exit 1
            fi
            echo "loki/receipt is now a required check on ${_pr_slug}@${_pr_branch} (was: ${_pr_mode}). Undo with: loki github protect --off"
            ;;

        help|*)
            echo -e "${BOLD}loki github${NC} - GitHub integration management"
            echo ""
            echo "Commands:"
            echo "  status       Show GitHub integration status"
            echo "  sync         Sync completed task status back to GitHub issues"
            echo "  export       Export local tasks as new GitHub issues"
            echo "  pr [name]    Create pull request from completed work"
            echo "  publish [name] [--public]  Push to GitHub and set a loki/receipt commit status"
            echo "  protect [--dry-run] [--off]  Require loki/receipt on the default branch (--off restores)"
            echo ""
            echo "Environment Variables:"
            echo "  LOKI_GITHUB_IMPORT=true    Import open issues as tasks on start"
            echo "  LOKI_GITHUB_SYNC=true      Sync status back to issues during session"
            echo "  LOKI_GITHUB_PR=true        Create PR when session completes successfully"
            echo "  LOKI_GITHUB_LABELS=bug     Filter issues by label (comma-separated)"
            echo "  LOKI_GITHUB_MILESTONE=v2   Filter by milestone"
            echo "  LOKI_GITHUB_ASSIGNEE=me    Filter by assignee"
            echo "  LOKI_GITHUB_LIMIT=50       Max issues to import (default: 100)"
            echo "  LOKI_GITHUB_PR_LABEL=loki  Label to add to created PRs"
            echo ""
            echo "Examples:"
            echo "  loki github status"
            echo "  loki github sync"
            echo "  loki github export"
            echo "  loki github pr \"Add user authentication\""
            echo "  LOKI_GITHUB_SYNC=true loki start --github ./prd.md"
            ;;
    esac
}

# Parse GitHub issue using issue-parser.sh
cmd_issue_parse() {
    local issue_ref=""
    local format="yaml"
    local output_file=""
    local quiet=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki issue parse${NC} - Parse GitHub issue into structured format"
                echo ""
                echo "Usage: loki issue parse <issue-ref> [options]"
                echo ""
                echo "Issue Reference Formats:"
                echo "  https://github.com/owner/repo/issues/123"
                echo "  owner/repo#123"
                echo "  #123                  (uses current repo)"
                echo "  123                   (uses current repo)"
                echo ""
                echo "Options:"
                echo "  --format yaml|json    Output format (default: yaml)"
                echo "  --output FILE         Write to file instead of stdout"
                echo "  --quiet               Suppress info messages"
                echo ""
                echo "Examples:"
                echo "  loki issue parse 123"
                echo "  loki issue parse owner/repo#456 --format json"
                echo "  loki issue parse 789 --output issue.yaml"
                exit 0
                ;;
            --format)
                format="${2:-yaml}"
                if [ $# -ge 2 ]; then shift 2; else shift; fi
                ;;
            --format=*)
                format="${1#*=}"
                shift
                ;;
            --output|-o)
                output_file="${2:-}"
                if [ $# -ge 2 ]; then shift 2; else shift; fi
                ;;
            --output=*)
                output_file="${1#*=}"
                if [[ "$output_file" == *".."* ]]; then
                    echo -e "${RED}Error: Output path must not contain '..'${NC}"
                    exit 1
                fi
                shift
                ;;
            --quiet|-q)
                quiet=true
                shift
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki issue parse --help' for usage."
                exit 1
                ;;
            *)
                if [[ -z "$issue_ref" ]]; then
                    issue_ref="$1"
                fi
                shift
                ;;
        esac
    done

    if [[ -z "$issue_ref" ]]; then
        echo -e "${RED}Error: Issue reference required${NC}"
        echo "Usage: loki issue parse <issue-ref>"
        exit 2
    fi

    # Find and run issue-parser.sh
    local parser_script="$SKILL_DIR/autonomy/issue-parser.sh"
    if [[ ! -f "$parser_script" ]]; then
        echo -e "${RED}Error: issue-parser.sh not found at $parser_script${NC}"
        exit 1
    fi

    local args=("$issue_ref" "--format" "$format")
    [[ -n "$output_file" ]] && args+=("--output" "$output_file")
    [[ "$quiet" == "true" ]] && args+=("--quiet")

    "$parser_script" "${args[@]}"
}

# View parsed GitHub issue details
cmd_issue_view() {
    require_jq || return 1

    local issue_ref="${1:-}"

    # Handle --help flag
    if [[ "$issue_ref" == "--help" || "$issue_ref" == "-h" ]]; then
        echo -e "${BOLD}loki issue view${NC} - View parsed GitHub issue details"
        echo ""
        echo "Usage: loki issue view <issue-ref>"
        echo ""
        echo "Issue Reference Formats:"
        echo "  https://github.com/owner/repo/issues/123"
        echo "  owner/repo#123"
        echo "  #123                  (uses current repo)"
        echo "  123                   (uses current repo)"
        echo ""
        echo "Examples:"
        echo "  loki issue view 123"
        echo "  loki issue view owner/repo#456"
        exit 0
    fi

    if [[ -z "$issue_ref" ]]; then
        echo -e "${RED}Error: Issue reference required${NC}"
        echo "Usage: loki issue view <issue-ref>"
        exit 2
    fi

    # Find and run issue-parser.sh
    local parser_script="$SKILL_DIR/autonomy/issue-parser.sh"
    if [[ ! -f "$parser_script" ]]; then
        echo -e "${RED}Error: issue-parser.sh not found at $parser_script${NC}"
        exit 1
    fi

    # Parse and get JSON for easier extraction
    local parsed_json
    parsed_json=$("$parser_script" "$issue_ref" --format json --quiet 2>&1)
    local exit_code=$?

    if [[ $exit_code -ne 0 ]]; then
        echo "$parsed_json"
        exit $exit_code
    fi

    # Extract fields using jq
    local title type priority labels url problem acceptance technical raw_body
    title=$(echo "$parsed_json" | jq -r '.metadata.title // ""')
    type=$(echo "$parsed_json" | jq -r '.metadata.type // "task"')
    priority=$(echo "$parsed_json" | jq -r '.metadata.priority // "normal"')
    labels=$(echo "$parsed_json" | jq -r '.metadata.labels | join(", ") // ""')
    url=$(echo "$parsed_json" | jq -r '.source.url // ""')
    problem=$(echo "$parsed_json" | jq -r '.content.problem_statement // ""')
    acceptance=$(echo "$parsed_json" | jq -r '.content.acceptance_criteria // ""')
    technical=$(echo "$parsed_json" | jq -r '.content.technical_requirements // ""')
    raw_body=$(echo "$parsed_json" | jq -r '.content.raw_body // ""')

    # Display structured view
    echo -e "${BOLD}GitHub Issue Details${NC}"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""

    echo -e "${CYAN}Title:${NC}    $title"
    echo -e "${CYAN}Type:${NC}     $type"
    echo -e "${CYAN}Priority:${NC} $priority"
    echo -e "${CYAN}Labels:${NC}   $labels"
    echo -e "${CYAN}URL:${NC}      $url"
    echo ""

    # Problem statement
    if [[ -n "$problem" ]]; then
        echo -e "${BOLD}Problem Statement${NC}"
        echo "─────────────────────────────────────────────────────────────────────────"
        echo "$problem" | head -15
        echo ""
    fi

    # Acceptance criteria
    if [[ -n "$acceptance" && "$acceptance" != "null" ]]; then
        echo -e "${BOLD}Acceptance Criteria${NC}"
        echo "─────────────────────────────────────────────────────────────────────────"
        echo "$acceptance" | head -15
        echo ""
    fi

    # Technical requirements
    if [[ -n "$technical" && "$technical" != "null" ]]; then
        echo -e "${BOLD}Technical Requirements${NC}"
        echo "─────────────────────────────────────────────────────────────────────────"
        echo "$technical" | head -10
        echo ""
    fi

    # Show raw body preview if nothing else was extracted
    if [[ -z "$problem" && -z "$acceptance" && -z "$technical" && -n "$raw_body" ]]; then
        echo -e "${BOLD}Issue Body${NC}"
        echo "─────────────────────────────────────────────────────────────────────────"
        echo "$raw_body" | head -20
        echo ""
    fi

    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""
    echo -e "Next steps:"
    echo -e "  ${CYAN}loki issue parse $issue_ref --format json${NC}  # Get JSON output"
    echo -e "  ${CYAN}loki issue $issue_ref --start${NC}              # Generate PRD and start"
}

#===============================================================================
# loki run - Issue-driven engineering (v6.0.0)
# Primary entry point for issue-to-implementation workflow.
# Supports GitHub, GitLab, Jira, and Azure DevOps issues.
#===============================================================================

cmd_run() {
    # Wall-clock anchor for the issue journey. Captured before argument parsing,
    # auth, or issue fetch so the first-result measurement includes the wait the
    # user actually experiences. Exported only through the explicit writer call
    # below; it does not alter runner/provider behavior.
    local _issue_journey_started_epoch
    _issue_journey_started_epoch="$(date +%s)"
    # v6.84.0: `loki run` is now a deprecated alias for `loki start <issue-ref>`.
    # When invoked directly (not via the unified start dispatcher), emit a
    # deprecation notice and a telemetry event so we can track adoption of the
    # unified command. Skip when LOKI_UNIFIED_START=1 (cmd_start -> cmd_run).
    if [ "${LOKI_UNIFIED_START:-0}" != "1" ]; then
        # Only warn for real invocations, not --help (help is still useful)
        local _show_warn=true
        for _a in "$@"; do
            if [ "$_a" = "--help" ] || [ "$_a" = "-h" ]; then
                _show_warn=false
                break
            fi
        done
        # CLI consolidation (Phase A): align the run->start notice with the
        # standardized one-line alias contract (stderr-only, suppressed under
        # --json/-q/--quiet, contains "is now 'loki start'"). The word
        # "deprecated" is kept so existing run/start unification tests still
        # match. Telemetry reuses the cli_command_deprecated event.
        if [ "$_show_warn" = "true" ] && ! _deprecated_alias_should_suppress "$@"; then
            echo "note: 'loki run' is now 'loki start <issue-ref>' (run is deprecated). The old form still works." >&2
            # Emit telemetry event so we can measure adoption of the unified command.
            # Signature: emit_event <type> <source> <action> [key=value ...]
            # No-side-effect alias contract (matches _deprecated_alias at :371):
            # events/emit.sh creates "$LOKI_DIR/events/pending" as a side effect,
            # which would make a clean directory suddenly contain .loki and flip
            # downstream guards (cmd_share's "No .loki/" check, bash-vs-bun read
            # divergence). Only emit when .loki already exists; the stderr pointer
            # is always printed (modulo machine-output suppression above).
            if [ -d "${LOKI_DIR:-.loki}" ]; then
                emit_event cli_command_deprecated cli run_to_start \
                    "from=run" \
                    "to=start" \
                    "version=6.84.0" \
                    "argv=${*:-}"
            fi
        fi
    fi

    require_jq || return 1

    local issue_ref=""
    local dry_run=false
    local output_file=""
    local start_args=()
    local no_start=false
    local provider_override=""
    local use_worktree=false
    local create_pr=false
    local prepare_pr=false
    local auto_merge=false
    local run_detached=false
    # Internal: set by cmd_start when a one-line brief carries --pr/--worktree/
    # --detach. The brief is treated as a tracker-less issue so it reuses the
    # worktree, detach and PR machinery below instead of a parallel copy.
    local brief_in=""

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki run${NC} - Issue-driven engineering (DEPRECATED: use 'loki start')"
                echo ""
                echo "Usage: loki run <issue-ref> [options]"
                echo "       loki run <url-or-key> [options]"
                echo ""
                echo "Takes an issue from any supported tracker, generates a PRD,"
                echo "and starts Loki Mode to implement it autonomously."
                echo ""
                echo "Issue Reference Formats:"
                echo "  GitHub:       123, #123, owner/repo#123, https://github.com/..."
                echo "  GitLab:       https://gitlab.com/owner/repo/-/issues/42"
                echo "  Jira:         PROJ-123, https://org.atlassian.net/browse/PROJ-123"
                echo "  Azure DevOps: https://dev.azure.com/org/project/_workitems/edit/456"
                echo ""
                echo "Options:"
                echo "  --dry-run          Preview generated PRD without starting"
                echo "  --no-start         Generate PRD but don't start execution"
                echo "  --output FILE      Save PRD to custom path"
                echo "  --provider NAME    AI provider: claude (default), codex, cline, aider, opencode"
                echo "  --parallel         Enable parallel mode with git worktrees"
                echo "  --bg, --background Run in background mode"
                echo "  --simple           Force simple complexity tier"
                echo "  --complex          Force complex complexity tier"
                echo "  --no-dashboard     Disable web dashboard"
                echo "  --sandbox          Run in Docker sandbox (default: off; requires Docker)"
                echo "  --no-plan          Skip auto-shown PRD analysis at startup"
                echo "  --budget USD       Per-run cost cap (default $(loki_budget_default_usd))"
                echo "  --brief-text TEXT  (internal) set by 'loki start --brief'; not for direct use"
                echo ""
                echo "Progressive Isolation:"
                echo "  --worktree, -w     Git worktree isolation (separate branch)"
                echo "  --pr               Worktree + auto-create PR (implies --worktree)"
                echo "  --prepare-pr       Worktree + write a review-ready PR title/body"
                echo "                     locally; never contacts GitHub (implies --worktree)"
                echo "  --ship             Worktree + PR + auto-merge (implies --pr)"
                echo "  --detach, -d       Run in background (implies --worktree)"
                echo ""
                echo "  Cascade: --ship implies --pr implies --worktree"
                echo ""
                echo "Environment Variables:"
                echo "  JIRA_API_TOKEN     Jira API token (for Jira issues)"
                echo "  JIRA_URL           Jira base URL (for Jira issues)"
                echo "  JIRA_EMAIL         Jira user email (for Jira Cloud auth)"
                echo ""
                echo "Examples:"
                echo "  loki run 123                             # GitHub issue from current repo"
                echo "  loki run owner/repo#456                  # GitHub issue from specific repo"
                echo "  loki run PROJ-789                        # Jira issue"
                echo "  loki run https://gitlab.com/o/r/-/issues/42  # GitLab issue"
                echo "  loki run 123 --dry-run                   # Preview PRD"
                echo "  loki run 123 --parallel --provider codex # Parallel mode with Codex"
                echo "  loki run 123 --worktree                  # Isolated branch"
                echo "  loki run 123 --pr                        # Auto-create PR"
                echo "  loki run 123 --ship                      # Full automation: PR + merge"
                echo "  loki run 123 --ship -d                   # Background, full automation"
                exit 0
                ;;
            --dry-run)
                dry_run=true
                shift
                ;;
            --no-start)
                no_start=true
                shift
                ;;
            --output)
                if [[ -n "${2:-}" ]]; then
                    output_file="$2"
                    if [[ "$output_file" == *".."* ]]; then
                        echo -e "${RED}Error: Output path must not contain '..'${NC}"
                        exit 1
                    fi
                    shift 2
                else
                    echo -e "${RED}--output requires a file path${NC}"
                    exit 1
                fi
                ;;
            --output=*)
                output_file="${1#*=}"
                if [[ "$output_file" == *".."* ]]; then
                    echo -e "${RED}Error: Output path must not contain '..'${NC}"
                    exit 1
                fi
                shift
                ;;
            --provider)
                if [[ -n "${2:-}" ]]; then
                    provider_override="$2"
                    start_args+=("--provider" "$2")
                    # UT2-13: Record CLI provider at parse time (atomic write).
                    # v7.7.11 (council fix): validate against canonical set; include
                    # PID so reader can drop the marker when originating process is gone.
                    case "$2" in
                        claude|codex|cline|aider|opencode)
                            mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
                            printf '%s:%s:%s\n' "$2" "$(date +%s)" "$$" \
                                > "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
                                && mv "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" \
                                      "${LOKI_DIR:-.loki}/state/cli-provider" 2>/dev/null || true
                            ;;
                    esac
                    shift 2
                else
                    echo -e "${RED}--provider requires a value${NC}"
                    exit 1
                fi
                ;;
            --provider=*)
                provider_override="${1#*=}"
                start_args+=("--provider" "$provider_override")
                # UT2-13: Record CLI provider at parse time (atomic write).
                # v7.7.11 (council fix): validate + include PID.
                case "$provider_override" in
                    claude|codex|cline|aider|opencode)
                        mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
                        printf '%s:%s:%s\n' "$provider_override" "$(date +%s)" "$$" \
                            > "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
                            && mv "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" \
                                  "${LOKI_DIR:-.loki}/state/cli-provider" 2>/dev/null || true
                        ;;
                esac
                shift
                ;;
            --parallel|--bg|--background|--simple|--complex|--no-dashboard|--sandbox|--no-plan)
                start_args+=("$1")
                shift
                ;;
            --budget)
                if [[ -n "${2:-}" ]]; then
                    start_args+=("--budget" "$2")
                    shift 2
                else
                    echo -e "${RED}--budget requires a USD amount${NC}"
                    exit 1
                fi
                ;;
            --budget=*)
                start_args+=("--budget" "${1#*=}")
                shift
                ;;
            --worktree|-w)
                use_worktree=true
                shift
                ;;
            --pr)
                use_worktree=true
                create_pr=true
                shift
                ;;
            --prepare-pr)
                # Golden path (feature 9): produce a review-ready PR WITHOUT any
                # GitHub mutation. Writes the title/body to .loki/state/ and
                # records state="prepared" for the receipt. Deliberately does not
                # set create_pr: consent to prepare is not consent to publish.
                use_worktree=true
                prepare_pr=true
                shift
                ;;
            --ship)
                use_worktree=true
                create_pr=true
                auto_merge=true
                shift
                ;;
            --detach|-d)
                use_worktree=true
                run_detached=true
                shift
                ;;
            # #691: unified config-file aliases. Applied by the main() pre-pass;
            # consume here so the path is not misread as the issue-ref positional
            # and is not forwarded to cmd_start.
            --config|--vars|--env-file)
                # Guard shift 2 against a dangling flag (silent set -e abort).
                if [ "$#" -ge 2 ]; then
                    shift 2
                else
                    echo "loki: $1 requires a file path (e.g. --config .loki/config.yaml)" >&2
                    exit 1
                fi
                ;;
            --config=*|--vars=*|--env-file=*)
                shift
                ;;
            --brief-text)
                if [ "$#" -ge 2 ]; then
                    brief_in="$2"
                    shift 2
                else
                    echo -e "${RED}--brief-text requires text${NC}"
                    exit 1
                fi
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki run --help' for usage."
                exit 1
                ;;
            *)
                if [[ -z "$issue_ref" ]]; then
                    issue_ref="$1"
                fi
                shift
                ;;
        esac
    done

    # Validate git prerequisites for --ship and --pr flags
    if $create_pr || $auto_merge; then
        if ! command -v git &>/dev/null; then
            echo -e "${RED}Error: --pr/--ship requires git but it is not installed.${NC}"
            return 1
        fi
        if ! git rev-parse --git-dir &>/dev/null 2>&1; then
            echo -e "${RED}Error: --pr/--ship requires a git repository but the current directory is not one.${NC}"
            return 1
        fi
        if [[ -z "$(git remote 2>/dev/null)" ]]; then
            echo -e "${RED}Error: --pr/--ship requires a git remote but none is configured.${NC}"
            return 1
        fi
    fi

    # Add --parallel once if worktree mode is enabled (not per-flag)
    if $use_worktree; then
        start_args+=("--parallel")
    fi

    if [[ -n "$brief_in" ]]; then
        issue_ref="brief"
    fi

    if [[ -z "$issue_ref" ]]; then
        echo -e "${RED}Error: Issue reference required${NC}"
        echo ""
        echo "Usage: loki run <issue-ref> [options]"
        echo ""
        echo "Examples:"
        echo "  loki run 123                    # GitHub issue"
        echo "  loki run PROJ-456               # Jira issue"
        echo "  loki run owner/repo#789         # GitHub with specific repo"
        echo ""
        echo "Run 'loki run --help' for full usage."
        exit 2
    fi

    # Source issue provider abstraction
    local issue_providers_script="$SKILL_DIR/autonomy/issue-providers.sh"
    if [[ ! -f "$issue_providers_script" ]]; then
        echo -e "${RED}Error: issue-providers.sh not found at $issue_providers_script${NC}"
        exit 1
    fi
    source "$issue_providers_script"

    # Golden path: also source issue-parser.sh for write_journey_context and
    # extract_acceptance_criteria. Optional -- a missing file must never break a
    # run, so this is guarded rather than fatal like the block above.
    #
    # issue-parser.sh sets `set -euo pipefail` at the top, which would otherwise
    # LEAK into cmd_run and abort the CLI on the first benign non-zero. We
    # snapshot the current options and restore them immediately after sourcing.
    local _gp_parser_script="$SKILL_DIR/autonomy/issue-parser.sh"
    if [[ -f "$_gp_parser_script" ]]; then
        local _gp_saved_opts
        _gp_saved_opts=$(set +o)
        source "$_gp_parser_script" 2>/dev/null || true
        eval "$_gp_saved_opts" 2>/dev/null || true
    fi

    # Detect provider
    local issue_provider
    local issue_json
    if [[ -n "$brief_in" ]]; then
        # Brief: no tracker to fetch from. The text travels in env so quoting in
        # the brief can never reach the shell or python source.
        issue_provider="brief"
        echo -e "${CYAN}Source:${NC} one-line brief"
        issue_json=$(LOKI_BRIEF_IN="$brief_in" LOKI_BRIEF_TS="$(date +%s)" python3 -c "
import json, os
t = os.environ['LOKI_BRIEF_IN']
first = t.strip().splitlines()[0] if t.strip() else 'brief'
print(json.dumps({'provider': 'brief', 'number': 'b' + os.environ['LOKI_BRIEF_TS'],
    'title': first[:80], 'body': t, 'labels': [], 'url': '', 'repo': ''}))
") || {
            echo -e "${RED}Failed to build brief${NC}"
            exit 1
        }
    else
        issue_provider=$(detect_issue_provider "$issue_ref")
        echo -e "${CYAN}Issue provider:${NC} ${issue_provider}"

        # Fetch the issue
        echo -e "${CYAN}Fetching issue...${NC}"
        issue_json=$(fetch_issue "$issue_ref") || {
            echo -e "${RED}Failed to fetch issue${NC}"
            exit 1
        }
    fi

    # Extract fields for display
    local title number url
    title=$(echo "$issue_json" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('title',''))")
    number=$(echo "$issue_json" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('number',''))")
    url=$(echo "$issue_json" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('url',''))")

    echo -e "${GREEN}Issue #$number:${NC} $title"
    if [[ -n "$url" ]]; then
        echo -e "${DIM}$url${NC}"
    fi
    echo ""

    # Golden path (features 2 + 3): capture the EXACT acceptance context and the
    # early machine-readable plan here, at the real `loki start <issue>`
    # chokepoint, BEFORE any provider call.
    #
    # This is deliberately hooked to fetch_issue rather than to
    # issue-parser.sh's parse_github_issue: `loki start` routes through
    # fetch_issue (issue-providers.sh:333), while parse_github_issue serves only
    # the DEPRECATED `loki issue parse|view` commands. Hooking the parser alone
    # would leave the required path with no context written at all. fetch_issue
    # is also provider-agnostic, so gitlab/jira/azure get the same artifacts.
    #
    # Best-effort: never allowed to break the run.
    if declare -f write_journey_context >/dev/null 2>&1; then
        # owner/repo come from the NORMALIZED json's `repo` field ("owner/name"),
        # not from ISSUE_OWNER/ISSUE_REPO: those globals are re-cleared by any
        # later parse_issue_reference call, which yielded a "/#42" ref here.
        # Every provider populates `repo`, so this stays provider-agnostic.
        local _gp_owner="" _gp_repo="" _gp_acc="" _gp_body="" _gp_repo_ref=""
        _gp_repo_ref=$(printf '%s' "$issue_json" | python3 -c \
            "import json,sys; print(json.loads(sys.stdin.read()).get('repo','') or '')" 2>/dev/null || true)
        _gp_owner="${_gp_repo_ref%%/*}"
        _gp_repo="${_gp_repo_ref#*/}"
        [ "$_gp_owner" = "$_gp_repo_ref" ] && _gp_repo=""
        _gp_body=$(printf '%s' "$issue_json" | python3 -c \
            "import json,sys; print(json.loads(sys.stdin.read()).get('body','') or '')" 2>/dev/null || true)
        if declare -f extract_acceptance_criteria >/dev/null 2>&1; then
            _gp_acc=$(extract_acceptance_criteria "$_gp_body" 2>/dev/null || true)
        fi
        write_journey_context "$_gp_owner" "$_gp_repo" "${number:-}" "$title" \
            "$url" "$_gp_acc" "" "${issue_provider:-github}" "" \
            "$_issue_journey_started_epoch" 2>/dev/null || true
    fi

    # Per-session locking (v6.4.0): export session ID so run.sh uses
    # per-session PID/lock files instead of the global lock. This allows
    # multiple concurrent `loki run` sessions (e.g., loki run 52 -d && loki run 54 -d).
    local session_id="${number:-$(date +%s)}"
    export LOKI_SESSION_ID="$session_id"

    # Progressive isolation: set up worktree branch naming
    if $use_worktree; then
        local branch_name="issue/${issue_provider}-${number:-$(date +%s)}"
        log_info "Progressive isolation: branch $branch_name"
        export LOKI_PARALLEL_MODE=true
        export LOKI_WORKTREE_BRANCH="$branch_name"
    fi

    # Generate PRD
    local prd_content
    # Pass the REAL version through. The generator falls back to "unknown"
    # without it, and it used to hardcode v6.0.0 -- printing a three-majors-stale
    # version in the first artifact a user sees on the issue path.
    prd_content=$(echo "$issue_json" | LOKI_VERSION="$(get_version)" generate_prd_from_issue)

    # Detached mode: fork to background
    if $run_detached; then
        # Guard: prevent launching duplicate session
        if is_session_running "$session_id"; then
            echo -e "${RED}Error: Session '$session_id' is already running.${NC}"
            echo -e "Stop it first with: ${CYAN}loki stop $session_id${NC}"
            exit 1
        fi

        local log_file="$LOKI_DIR/logs/run-${number:-$(date +%s)}.log"
        mkdir -p "$(dirname "$log_file")"
        echo -e "${GREEN}Running detached. Logs: $log_file${NC}"
        echo -e "Check status: ${CYAN}loki status${NC}"

        # Write PRD first so background process can use it
        mkdir -p "$LOKI_DIR"
        local detach_prd="$LOKI_DIR/prd-issue-${number}.md"
        echo "$prd_content" > "$detach_prd"

        if [[ -z "${branch_name:-}" ]]; then
            branch_name="issue/detach-$(date +%s)"
        fi

        # Write a temp script to avoid shell injection via variable interpolation
        local run_script="$LOKI_DIR/scripts/run-${number:-detached}.sh"
        mkdir -p "$LOKI_DIR/scripts"
        local loki_cmd
        loki_cmd="$(command -v loki || echo "$0")"
        cat > "$run_script" << 'INNER_SCRIPT_EOF'
#!/usr/bin/env bash
set -euo pipefail
cd "$LOKI_RUN_DIR"
export LOKI_DETACHED=true
export LOKI_PARALLEL_MODE=true
"$LOKI_CMD" start "$LOKI_PRD_PATH" ${LOKI_START_ARGS:-}

# Post-completion: create PR if requested
if [[ "$LOKI_CREATE_PR" == "true" ]]; then
    branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
    if [[ -n "$branch_current" && "$branch_current" != "main" && "$branch_current" != "master" ]]; then
        git push origin "$branch_current" 2>/dev/null || true
        # Proven PR (v7.90.0): render this run's Evidence Receipt into the PR body
        # so even the detached --pr/--ship path proves itself. The proof + the
        # run-id pointer (.loki/state/last-proof-id.txt) exist by now (the
        # `loki start` above generated them). On by default (LOKI_PROVEN_PR=0
        # opts out); degrades safely to the plain body if anything is missing.
        # The renderer is print-only + honesty-gated (green only when the proof
        # headline is VERIFIED); it can never make a receipt-less PR look proven.
        _pr_body="Implemented by Loki Mode"
        if [[ "${LOKI_PROVEN_PR:-1}" != "0" ]]; then
            # Resolve the renderer from the ALREADY-RESOLVED autonomy dir passed
            # in by the parent (LOKI_SCRIPT_DIR_RESOLVED = _LOKI_SCRIPT_DIR, which
            # is symlink-safe). Do NOT recompute from `command -v loki` -- on
            # npm/bun/brew global installs that is an UNRESOLVED bin symlink, so
            # dirname-based guessing misses the lib and the receipt silently no-ops
            # on the dominant install channels (council cIb_r2). Fall back to the
            # old dirname guesses only if the resolved dir was not passed.
            _pp_lib="${LOKI_SCRIPT_DIR_RESOLVED:-}/lib/proof-pr.sh"
            [[ -f "$_pp_lib" ]] || _pp_lib="$(dirname "$LOKI_CMD")/../autonomy/lib/proof-pr.sh"
            [[ -f "$_pp_lib" ]] || _pp_lib="$(dirname "$LOKI_CMD")/autonomy/lib/proof-pr.sh"
            _pp_id=""
            [[ -f .loki/state/last-proof-id.txt ]] && _pp_id="$(cat .loki/state/last-proof-id.txt 2>/dev/null)"
            _pp_proof=".loki/proofs/${_pp_id}/proof.json"
            if [[ -n "$_pp_id" && -f "$_pp_proof" && -f "$_pp_lib" ]]; then
                # shellcheck source=/dev/null
                if source "$_pp_lib" 2>/dev/null && declare -f render_evidence_receipt_md >/dev/null 2>&1; then
                    _pp_block="$(render_evidence_receipt_md "$_pp_proof" "" "" 2>/dev/null || true)"
                    [[ -n "$_pp_block" ]] && _pr_body="Implemented by Loki Mode"$'\n\n'"$_pp_block"
                fi
            fi
        fi
        gh pr create --title "$LOKI_PR_TITLE" --body "$_pr_body" --head "$branch_current" 2>/dev/null || true
    fi
fi
# Post-completion: auto-merge if requested
if [[ "$LOKI_AUTO_MERGE" == "true" ]]; then
    branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
    if "$LOKI_CMD" merge watch "$branch_current" --require-verified --delete-branch; then
        # A brief run carries a synthetic ref (b<epoch>), not a real issue: close only numeric refs.
        if [[ "${LOKI_ISSUE_NUMBER:-}" =~ ^[0-9]+$ ]]; then
            gh issue close "$LOKI_ISSUE_NUMBER" --comment "Resolved by Loki Mode" 2>/dev/null || true
        fi
    fi
fi
INNER_SCRIPT_EOF
        chmod +x "$run_script"

        # Propagate the nested-agent signal into the detached run. CLAUDECODE (set
        # by Claude Code / the Loki Mode skill) is NOT in the curated env list the
        # inner script inherits, so run.sh's nested guard would never see it once
        # detached. Forward it as LOKI_NESTED_AGENT (which run.sh honors) so the
        # testing/docs worktree sub-streams stay OFF when nested even after the
        # nohup fork. Already-set LOKI_NESTED_AGENT wins.
        local _loki_nested_pass="${LOKI_NESTED_AGENT:-}"
        if [ -z "$_loki_nested_pass" ] && [ -n "${CLAUDECODE:-}" ]; then
            _loki_nested_pass=1
        fi
        # Pass all variables safely via environment
        LOKI_RUN_DIR="$(pwd)" \
        LOKI_CMD="$loki_cmd" \
        LOKI_SCRIPT_DIR_RESOLVED="$_LOKI_SCRIPT_DIR" \
        LOKI_SESSION_ID="$session_id" \
        LOKI_WORKTREE_BRANCH="$branch_name" \
        LOKI_PRD_PATH="$detach_prd" \
        LOKI_START_ARGS="${start_args[*]+"${start_args[*]}"}" \
        LOKI_CREATE_PR="$create_pr" \
        LOKI_AUTO_MERGE="$auto_merge" \
        LOKI_PR_TITLE="${title:-Implementation for issue ${issue_ref}}" \
        LOKI_ISSUE_NUMBER="${number:-}" \
        LOKI_NESTED_AGENT="$_loki_nested_pass" \
        nohup bash "$run_script" > "$log_file" 2>&1 &

        local bg_pid=$!
        echo "$bg_pid" > "$LOKI_DIR/run-${number:-detached}.pid"
        echo "Background PID: $bg_pid"
        return 0
    fi

    # Handle dry-run
    if [[ "$dry_run" == "true" ]]; then
        echo -e "${BOLD}Generated PRD Preview:${NC}"
        echo "------------------------------------------------------------------------"
        echo "$prd_content"
        echo "------------------------------------------------------------------------"
        echo ""
        echo -e "${DIM}(dry-run mode - PRD not saved)${NC}"
        exit 0
    fi

    # Determine output file
    if [[ -z "$output_file" ]]; then
        mkdir -p "$LOKI_DIR"
        # Use provider-specific naming
        case "$issue_provider" in
            github|gitlab) output_file="$LOKI_DIR/prd-issue-$number.md" ;;
            jira)          output_file="$LOKI_DIR/prd-$number.md" ;;
            azure_devops)  output_file="$LOKI_DIR/prd-ado-$number.md" ;;
            *)             output_file="$LOKI_DIR/prd-issue-$number.md" ;;
        esac
    fi

    # Write PRD file
    echo "$prd_content" > "$output_file"
    echo -e "${GREEN}PRD generated:${NC} $output_file"

    # v6.81.1: auto-show PRD analysis once the issue-derived PRD is on disk,
    # so users see cost / complexity / time before execution begins. When the
    # user will continue into `cmd_start` below, cmd_start also has its own
    # auto-plan call -- but that second call respects --no-plan / TTY checks
    # and would typically re-emit the same analysis. We therefore pass
    # --no-plan along in start_args to prevent duplicate output. Passing
    # --no-plan here is safe because `cmd_run` itself already displayed the
    # plan.
    local _run_no_plan=false
    for _arg in "${start_args[@]+"${start_args[@]}"}"; do
        if [[ "$_arg" == "--no-plan" ]]; then
            _run_no_plan=true
            break
        fi
    done
    if [ -f "$output_file" ]; then
        maybe_show_auto_plan "$output_file" "$_run_no_plan" || true
    fi
    # Ensure cmd_start does not re-show the plan now that we just did.
    if [[ "$_run_no_plan" != "true" ]]; then
        start_args+=("--no-plan")
    fi

    # Start Loki Mode unless --no-start
    if [[ "$no_start" == "true" ]]; then
        echo ""
        echo "Next steps:"
        echo -e "  ${CYAN}loki start $output_file${NC}    # Start with generated PRD"
        echo -e "  ${CYAN}cat $output_file${NC}            # View PRD"
    else
        echo ""
        echo -e "${GREEN}Starting Loki Mode with generated PRD...${NC}"
        # cmd_start EXEC-replaces this process (loki:3490
        # _loki_new_session_exec), so ANY code after a bare call is unreachable
        # -- which is why the --pr block below has never run on this path and
        # why the detached route at ~:10088 reimplements PR creation instead of
        # sharing it. Verified empirically, not inferred.
        #
        # Only --prepare-pr subshells the runner, so the exec replaces the CHILD
        # and control returns here. The default path keeps the bare call
        # untouched: subshelling everything would change signal delivery and
        # Ctrl+C semantics for every existing user, which this slice must not do.
        if $prepare_pr && ! $create_pr; then
            ( cmd_start "$output_file" ${start_args[@]+"${start_args[@]}"} )
        else
            cmd_start "$output_file" ${start_args[@]+"${start_args[@]}"}
        fi

        # Golden path (feature 9): prepare-only. Builds the SAME review-ready
        # title/body the --pr path would publish, writes it to .loki/state/, and
        # records state="prepared" for the receipt -- without ever invoking gh.
        # Consent to prepare is not consent to publish, so this branch is
        # deliberately separate from create_pr and never falls through to it.
        if $prepare_pr && ! $create_pr; then
            echo ""
            echo -e "${GREEN}Preparing pull request (no GitHub changes)...${NC}"
            local _pp_title="${title:-Implementation for issue ${issue_ref}}"
            local _pp_branch
            _pp_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
            local _pp_body="Implemented by Loki Mode (autonomous agent)

Issue: ${issue_ref}
Provider: ${issue_provider}

## Changes
$(git log --oneline "main..HEAD" 2>/dev/null || echo "See diff")"

            # Same receipt the published path appends, so a prepared PR body is
            # byte-for-byte what would be posted. Print-only and honesty-gated.
            if [ "${LOKI_PROVEN_PR:-1}" != "0" ] && declare -f render_evidence_receipt_md >/dev/null 2>&1; then
                local _pp_idfile=".loki/state/last-proof-id.txt"
                if [ -s "$_pp_idfile" ]; then
                    local _pp_rid=""
                    _pp_rid="$(cat "$_pp_idfile" 2>/dev/null || true)"
                    if [ -n "$_pp_rid" ] && [ -f ".loki/proofs/$_pp_rid/proof.json" ]; then
                        local _pp_receipt=""
                        _pp_receipt="$(render_evidence_receipt_md ".loki/proofs/$_pp_rid/proof.json" "" "" 2>/dev/null || true)"
                        [ -n "$_pp_receipt" ] && _pp_body="${_pp_body}

${_pp_receipt}"
                    fi
                fi
            fi

            mkdir -p .loki/state 2>/dev/null || true
            printf '%s\n' "$_pp_title" > .loki/state/pr-title.txt 2>/dev/null || true
            printf '%s\n' "$_pp_body" > .loki/state/pr-body.md 2>/dev/null || true
            if command -v jq >/dev/null 2>&1; then
                jq -n --arg branch "$_pp_branch" --arg title "$_pp_title" \
                    '{state:"prepared", url:"", branch:$branch, title:$title,
                      body_path:".loki/state/pr-body.md",
                      note:"written locally; GitHub was not contacted"}' \
                    > .loki/state/pr.json 2>/dev/null || true
            fi
            echo -e "${GREEN}PR prepared${NC} -- review .loki/state/pr-body.md"
            echo -e "${DIM}Publish it yourself: gh pr create --title \"\$(cat .loki/state/pr-title.txt)\" --body-file .loki/state/pr-body.md${NC}"
        fi

        # Progressive isolation: create PR
        if $create_pr; then
            echo ""
            echo -e "${GREEN}Creating pull request...${NC}"
            local pr_title="${title:-Implementation for issue ${issue_ref}}"
            local pr_body="Implemented by Loki Mode (autonomous agent)

Issue: ${issue_ref}
Provider: ${issue_provider}

## Changes
$(git log --oneline "main..HEAD" 2>/dev/null || echo "See diff")"

            # Proven PR (Loop 6): append the Evidence Receipt to the body. One
            # append covers BOTH the gh and glab branches below. Default-on;
            # LOKI_PROVEN_PR=0 -> body byte-identical to before. The proof lives
            # under the project .loki resolved from the run_id pointer; empty
            # expected_head_sha by design (R-DET-1 run_id pointer is the guard).
            if [ "${LOKI_PROVEN_PR:-1}" != "0" ] && declare -f render_evidence_receipt_md >/dev/null 2>&1; then
                local _gh_idfile=".loki/state/last-proof-id.txt"
                if [ -s "$_gh_idfile" ]; then
                    local _gh_rid=""
                    _gh_rid="$(cat "$_gh_idfile" 2>/dev/null || true)"
                    if [ -n "$_gh_rid" ] && [ -f ".loki/proofs/$_gh_rid/proof.json" ]; then
                        local _gh_receipt=""
                        _gh_receipt="$(render_evidence_receipt_md ".loki/proofs/$_gh_rid/proof.json" "" "" 2>/dev/null || true)"
                        if [ -n "$_gh_receipt" ]; then
                            pr_body="${pr_body}

${_gh_receipt}"
                        fi
                    fi
                fi
            fi

            case "${issue_provider:-github}" in
                github|brief)
                    if command -v gh &>/dev/null; then
                        local branch_current
                        branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
                        if [[ -n "$branch_current" && "$branch_current" != "main" && "$branch_current" != "master" ]]; then
                            git push origin "$branch_current" 2>/dev/null || true
                            local _pr_out=""
                            if _pr_out=$(gh pr create --title "$pr_title" --body "$pr_body" --head "$branch_current" 2>/dev/null); then
                                echo -e "${GREEN}PR created${NC}"
                                # Record the real outcome for the receipt. Only
                                # written on ACTUAL success, so a failed create
                                # can never leave a "created" claim behind.
                                mkdir -p .loki/state 2>/dev/null || true
                                if command -v jq >/dev/null 2>&1; then
                                    jq -n --arg url "$(printf '%s' "$_pr_out" | grep -Eo 'https://[^[:space:]]+' | head -1)" \
                                          --arg branch "$branch_current" \
                                        '{state:"created", url:$url, branch:$branch,
                                          merge:{outcome:"waiting", gate:"human_review", checks:"not recorded"}}' \
                                        > .loki/state/pr.json 2>/dev/null || true
                                fi
                            else
                                echo -e "${YELLOW}PR creation failed${NC}"
                            fi
                        fi
                    fi
                    ;;
                gitlab)
                    if command -v glab &>/dev/null; then
                        local branch_current
                        branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
                        if [[ -n "$branch_current" && "$branch_current" != "main" && "$branch_current" != "master" ]]; then
                            git push origin "$branch_current" 2>/dev/null || true
                            glab mr create --title "$pr_title" --description "$pr_body" --source-branch "$branch_current" 2>/dev/null && \
                                echo -e "${GREEN}MR created${NC}" || \
                                echo -e "${YELLOW}MR creation failed${NC}"
                        fi
                    fi
                    ;;
                *)
                    echo -e "${YELLOW}PR creation not supported for provider: $issue_provider${NC}"
                    ;;
            esac
        fi

        # Progressive isolation: auto-merge
        if $auto_merge; then
            echo -e "${GREEN}Auto-merging...${NC}"
            case "${issue_provider:-github}" in
                github|brief)
                    local branch_current
                    branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
                    if _merge_pr_watch "$branch_current" --require-verified --delete-branch; then
                        echo -e "${GREEN}PR merged and branch deleted${NC}"
                        if [[ "${number:-}" =~ ^[0-9]+$ ]]; then
                            gh issue close "$number" --comment "Resolved by Loki Mode" 2>/dev/null || true
                        fi
                    else
                        echo -e "${YELLOW}Auto-merge failed. PR remains open.${NC}"
                    fi
                    ;;
                gitlab)
                    glab mr merge --squash --remove-source-branch 2>/dev/null && \
                        echo -e "${GREEN}MR merged and branch deleted${NC}" || \
                        echo -e "${YELLOW}Auto-merge failed. MR remains open.${NC}"
                    ;;
                *)
                    echo -e "${YELLOW}Auto-merge not supported for provider: $issue_provider${NC}"
                    ;;
            esac
        fi
    fi
}

# Generate PRD from GitHub issue (DEPRECATED in v6.0.0 - use 'loki run' instead)
cmd_issue() {
    # v6.0.0: Show deprecation notice
    echo -e "${YELLOW}[DEPRECATED] 'loki issue' is deprecated in v6.0.0. Use 'loki run' instead.${NC}" >&2
    echo -e "${YELLOW}  'loki run' supports GitHub, GitLab, Jira, and Azure DevOps issues.${NC}" >&2
    echo "" >&2

    require_jq || return 1

    local issue_ref=""
    local repo=""
    local number=""
    local start_loki=false
    local dry_run=false
    local output_file=""
    local start_args=()
    local subcommand=""

    # Check for subcommands first
    if [[ $# -gt 0 ]]; then
        case "$1" in
            parse)
                # Delegate to issue-parser.sh
                shift
                cmd_issue_parse "$@"
                return $?
                ;;
            view)
                # View parsed issue details
                shift
                cmd_issue_view "$@"
                return $?
                ;;
        esac
    fi

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki issue${NC} - Generate PRD from GitHub issue"
                echo ""
                echo "Usage: loki issue <url-or-number> [options]"
                echo "       loki issue parse <url-or-number> [--format yaml|json]"
                echo "       loki issue view <url-or-number>"
                echo "       loki issue --repo owner/repo --number 123 [options]"
                echo ""
                echo "Subcommands:"
                echo "  parse              Parse issue and output structured YAML/JSON"
                echo "  view               View parsed issue details"
                echo ""
                echo "Arguments:"
                echo "  <url-or-number>    GitHub issue URL or issue number"
                echo ""
                echo "Options:"
                echo "  --repo OWNER/REPO  Specify repository (default: auto-detect from git)"
                echo "  --number NUM       Specify issue number (alternative to positional arg)"
                echo "  --start            Start Loki Mode with the generated PRD"
                echo "  --dry-run          Preview generated PRD without saving"
                echo "  --output FILE      Save PRD to custom path (default: .loki/prd-issue-N.md)"
                echo ""
                echo "Options passed to 'start' (when --start is used):"
                echo "  --provider NAME    AI provider: claude (default), codex, cline, aider, opencode"
                echo "  --parallel         Enable parallel mode with git worktrees"
                echo "  --bg, --background Run in background mode"
                echo ""
                echo "Examples:"
                echo "  loki issue 123                           # Generate PRD from issue #123"
                echo "  loki issue 123 --dry-run                 # Preview without saving"
                echo "  loki issue 123 --start                   # Generate and start Loki Mode"
                echo "  loki issue 123 --start --parallel        # Generate, start in parallel mode"
                echo "  loki issue parse owner/repo#123          # Parse and output structured data"
                echo "  loki issue parse 123 --format json       # Parse and output JSON"
                echo "  loki issue view 123                      # View parsed issue details"
                echo "  loki issue --repo org/repo --number 456  # Specify repo and issue"
                echo "  loki issue https://github.com/org/repo/issues/789"
                exit 0
                ;;
            --repo)
                if [[ -n "${2:-}" ]]; then
                    repo="$2"
                    # Validate owner/repo format
                    if [[ ! "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
                        echo -e "${RED}Invalid --repo format: $repo${NC}"
                        echo "Expected: owner/repo (e.g., asklokesh/loki-mode)"
                        exit 1
                    fi
                    shift 2
                else
                    echo -e "${RED}--repo requires a value (owner/repo)${NC}"
                    exit 1
                fi
                ;;
            --repo=*)
                repo="${1#*=}"
                # Validate owner/repo format
                if [[ ! "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
                    echo -e "${RED}Invalid --repo format: $repo${NC}"
                    echo "Expected: owner/repo (e.g., asklokesh/loki-mode)"
                    exit 1
                fi
                shift
                ;;
            --number)
                if [[ -n "${2:-}" ]]; then
                    number="$2"
                    # Validate numeric
                    if [[ ! "$number" =~ ^[0-9]+$ ]]; then
                        echo -e "${RED}Invalid --number value: $number${NC}"
                        echo "Expected: numeric issue number (e.g., 123)"
                        exit 1
                    fi
                    shift 2
                else
                    echo -e "${RED}--number requires a value${NC}"
                    exit 1
                fi
                ;;
            --number=*)
                number="${1#*=}"
                # Validate numeric
                if [[ ! "$number" =~ ^[0-9]+$ ]]; then
                    echo -e "${RED}Invalid --number value: $number${NC}"
                    echo "Expected: numeric issue number (e.g., 123)"
                    exit 1
                fi
                shift
                ;;
            --start)
                start_loki=true
                shift
                ;;
            --dry-run)
                dry_run=true
                shift
                ;;
            --output)
                if [[ -n "${2:-}" ]]; then
                    output_file="$2"
                    if [[ "$output_file" == *".."* ]]; then
                        echo -e "${RED}Error: Output path must not contain '..'${NC}"
                        exit 1
                    fi
                    shift 2
                else
                    echo -e "${RED}--output requires a file path${NC}"
                    exit 1
                fi
                ;;
            --output=*)
                output_file="${1#*=}"
                if [[ "$output_file" == *".."* ]]; then
                    echo -e "${RED}Error: Output path must not contain '..'${NC}"
                    exit 1
                fi
                shift
                ;;
            # Pass through options to start command
            --provider|--parallel|--bg|--background|--simple|--complex|--github|--no-dashboard|--sandbox)
                start_args+=("$1")
                if [[ "$1" == "--provider" && -n "${2:-}" ]]; then
                    start_args+=("$2")
                    shift
                fi
                shift
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki issue --help' for usage."
                exit 1
                ;;
            *)
                issue_ref="$1"
                shift
                ;;
        esac
    done

    # Check gh CLI
    if ! command -v gh &> /dev/null; then
        echo -e "${RED}Error: gh CLI not found${NC}"
        echo "Install with: brew install gh"
        exit 1
    fi

    if ! gh auth status &> /dev/null 2>&1; then
        echo -e "${RED}Error: gh CLI not authenticated${NC}"
        echo "Run: gh auth login"
        exit 1
    fi

    # Parse issue reference
    if [[ -n "$issue_ref" ]]; then
        # Check if it's a URL
        if [[ "$issue_ref" =~ ^https?://github\.com/([^/]+/[^/]+)/issues/([0-9]+) ]]; then
            repo="${BASH_REMATCH[1]}"
            number="${BASH_REMATCH[2]}"
        elif [[ "$issue_ref" =~ ^([^/]+/[^/]+)#([0-9]+)$ ]]; then
            # owner/repo#number format
            repo="${BASH_REMATCH[1]}"
            number="${BASH_REMATCH[2]}"
        elif [[ "$issue_ref" =~ ^#?([0-9]+)$ ]]; then
            # Just a number (with optional #)
            number="${BASH_REMATCH[1]}"
        else
            echo -e "${RED}Invalid issue reference: $issue_ref${NC}"
            echo "Expected: issue number (123), owner/repo#123, or GitHub URL"
            exit 1
        fi
    fi

    # Validate we have a number
    if [[ -z "$number" ]]; then
        echo -e "${RED}Error: No issue number specified${NC}"
        echo "Usage: loki issue <number> or loki issue --number <number>"
        echo "Run 'loki issue --help' for full usage."
        exit 2
    fi

    # Auto-detect repo if not specified
    if [[ -z "$repo" ]]; then
        # Try to get from gh CLI (uses git remote)
        repo=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null) || true
        if [[ -z "$repo" ]]; then
            echo -e "${RED}Error: Could not determine repository${NC}"
            echo "Specify with: --repo owner/repo"
            echo "Or run from a directory with a GitHub remote configured."
            exit 1
        fi
    fi

    echo -e "${CYAN}Fetching issue #$number from $repo...${NC}"

    # Fetch issue details
    local issue_json
    if ! issue_json=$(gh issue view "$number" --repo "$repo" --json number,title,body,labels,milestone,author,createdAt,url 2>&1); then
        echo -e "${RED}Error fetching issue: $issue_json${NC}"
        exit 1
    fi

    # Extract fields
    local title body labels milestone author created_at issue_url
    title=$(echo "$issue_json" | jq -r '.title')
    body=$(echo "$issue_json" | jq -r '.body // ""')
    labels=$(echo "$issue_json" | jq -r '[.labels[].name] | join(", ") // ""')
    milestone=$(echo "$issue_json" | jq -r '.milestone.title // ""')
    author=$(echo "$issue_json" | jq -r '.author.login // ""')
    created_at=$(echo "$issue_json" | jq -r '.createdAt // ""')
    issue_url=$(echo "$issue_json" | jq -r '.url')

    # Generate PRD content
    local prd_content
    prd_content=$(cat <<EOF
# PRD: $title

**Source:** GitHub Issue [#$number]($issue_url)
**Author:** @$author
**Created:** $created_at
$([ -n "$labels" ] && echo "**Labels:** $labels")
$([ -n "$milestone" ] && echo "**Milestone:** $milestone")

---

## Overview

$body

---

## Acceptance Criteria

Based on the issue description, implement the following:

1. Address all requirements specified in the issue body above
2. Ensure backward compatibility (unless explicitly breaking changes are requested)
3. Add appropriate tests for new functionality
4. Update documentation as needed

---

## Technical Notes

- Source: GitHub Issue #$number
- Repository: $repo
- Generated by: Loki Mode CLI

---

## References

- Original Issue: $issue_url
EOF
)

    # Handle dry-run
    if [[ "$dry_run" == "true" ]]; then
        echo ""
        echo -e "${BOLD}Generated PRD Preview:${NC}"
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo "$prd_content"
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo ""
        echo -e "${DIM}(dry-run mode - PRD not saved)${NC}"
        exit 0
    fi

    # Determine output file
    if [[ -z "$output_file" ]]; then
        mkdir -p "$LOKI_DIR"
        output_file="$LOKI_DIR/prd-issue-$number.md"
    fi

    # Write PRD file
    echo "$prd_content" > "$output_file"
    echo -e "${GREEN}PRD generated: $output_file${NC}"

    # Fetch comments if any exist
    local comments_count
    comments_count=$(gh issue view "$number" --repo "$repo" --json comments -q '.comments | length' 2>/dev/null) || comments_count=0

    if [[ "$comments_count" -gt 0 ]]; then
        echo -e "${DIM}Issue has $comments_count comment(s) - view with: gh issue view $number --repo $repo${NC}"
    fi

    # Start Loki Mode if requested
    if [[ "$start_loki" == "true" ]]; then
        echo ""
        echo -e "${GREEN}Starting Loki Mode with generated PRD...${NC}"
        cmd_start "$output_file" ${start_args[@]+"${start_args[@]}"}
    else
        echo ""
        echo "Next steps:"
        echo -e "  ${CYAN}loki start $output_file${NC}           # Start Loki Mode with this PRD"
        echo -e "  ${CYAN}loki issue $number --start${NC}        # Or re-run with --start flag"
        echo -e "  ${DIM}cat $output_file${NC}  # View the generated PRD"
    fi
}

# Show configuration
#===============================================================================
# loki watch - Auto-rerun on PRD file changes (v6.33.0)
#===============================================================================

cmd_watch() {
    local prd_path=""
    local run_once=false
    local poll_interval=2
    local no_auto_start=false
    local debounce=3
    local _watch_child_pid=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki watch${NC} - Auto-rerun on PRD file changes"
                echo ""
                echo "Usage: loki watch [prd-path] [options]"
                echo ""
                echo "Monitors a PRD file for changes and automatically re-runs Loki Mode"
                echo "when the file is saved. Enables a tight edit-PRD-see-results loop."
                echo ""
                echo "Arguments:"
                echo "  prd-path            Path to PRD file (default: auto-detect prd.md, PRD.md, or first *.md)"
                echo ""
                echo "Options:"
                echo "  --once              Run once immediately then exit"
                echo "  --interval N        Poll interval in seconds for fallback watcher (default: 2)"
                echo "  --no-auto-start     Watch but do not auto-start, just print change timestamps"
                echo "  --debounce N        Wait N seconds after change before triggering (default: 3)"
                echo "  --help, -h          Show this help"
                echo ""
                echo "File watcher priority:"
                echo "  1. fswatch (macOS)    -- native filesystem events"
                echo "  2. inotifywait (Linux) -- inotify-based"
                echo "  3. stat polling        -- universal fallback"
                echo ""
                echo "Examples:"
                echo "  loki watch                       # Auto-detect PRD and watch"
                echo "  loki watch ./my-prd.md           # Watch specific file"
                echo "  loki watch --once                # Run once immediately then exit"
                echo "  loki watch --no-auto-start       # Just report changes, don't run"
                echo "  loki watch --debounce 5          # Wait 5s after change before triggering"
                echo "  loki watch --interval 1          # Poll every 1s (fallback mode)"
                return 0
                ;;
            --once) run_once=true; shift ;;
            --interval) poll_interval="${2:-2}"; [ $# -ge 2 ] && shift 2 || shift ;;
            --interval=*) poll_interval="${1#*=}"; shift ;;
            --no-auto-start) no_auto_start=true; shift ;;
            --debounce) debounce="${2:-3}"; [ $# -ge 2 ] && shift 2 || shift ;;
            --debounce=*) debounce="${1#*=}"; shift ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki watch --help' for usage."
                return 1
                ;;
            *)
                if [ -z "$prd_path" ]; then
                    prd_path="$1"
                else
                    echo -e "${RED}Unexpected argument: $1${NC}"
                    return 1
                fi
                shift
                ;;
        esac
    done

    # Validate numeric arguments (no leading zeros, no decimals, positive integers only)
    if ! [[ "$poll_interval" =~ ^[1-9][0-9]*$ ]]; then
        echo -e "${RED}Invalid --interval: $poll_interval (expected positive integer)${NC}"
        return 1
    fi
    if ! [[ "$debounce" =~ ^[1-9][0-9]*$ ]] && [ "$debounce" != "0" ]; then
        echo -e "${RED}Invalid --debounce: $debounce (expected non-negative integer)${NC}"
        return 1
    fi

    # Auto-detect PRD file if not specified
    if [ -z "$prd_path" ]; then
        if [ -f "prd.md" ]; then
            prd_path="prd.md"
        elif [ -f "PRD.md" ]; then
            prd_path="PRD.md"
        else
            # Find first .md file in current directory
            local first_md
            first_md=$(ls -1 *.md 2>/dev/null | head -1 || true)
            if [ -n "$first_md" ]; then
                prd_path="$first_md"
            else
                echo -e "${RED}No PRD file found.${NC}"
                echo "Specify a file: loki watch ./my-prd.md"
                echo "Or create one: prd.md, PRD.md, or any .md file in current directory."
                return 1
            fi
        fi
    fi

    if [ ! -f "$prd_path" ]; then
        echo -e "${RED}PRD file not found: $prd_path${NC}"
        return 1
    fi

    # Verify the PRD file is readable (#67)
    if [ ! -r "$prd_path" ]; then
        echo -e "${RED}PRD file is not readable: $prd_path${NC}"
        echo "Check file permissions: ls -la $prd_path"
        return 1
    fi

    # Resolve to absolute path
    prd_path="$(cd "$(dirname "$prd_path")" && pwd)/$(basename "$prd_path")"
    local prd_basename
    prd_basename="$(basename "$prd_path")"

    # --once mode: run immediately and exit
    if [ "$run_once" = true ]; then
        echo -e "${BOLD}loki watch${NC} --once: running loki start ${prd_basename}"
        "$0" start "$prd_path"
        return $?
    fi

    # Graceful shutdown handler
    _watch_cleanup() {
        echo ""
        echo "Watch stopped."
        if [ -n "$_watch_child_pid" ] && kill -0 "$_watch_child_pid" 2>/dev/null; then
            echo "Stopping running loki session (PID $_watch_child_pid)..."
            kill "$_watch_child_pid" 2>/dev/null
            wait "$_watch_child_pid" 2>/dev/null || true
        fi
        # Kill any background watcher process
        if [ -n "${_watch_bg_pid:-}" ] && kill -0 "$_watch_bg_pid" 2>/dev/null; then
            kill "$_watch_bg_pid" 2>/dev/null
            wait "$_watch_bg_pid" 2>/dev/null || true
        fi
        exit 0
    }
    trap '_watch_cleanup' INT TERM

    local last_run="never"

    echo -e "${BOLD}loki watch${NC} -- monitoring ${prd_basename} for changes"
    echo "Watching: $prd_path"
    echo "Debounce: ${debounce}s | Ctrl+C to exit"
    if [ "$no_auto_start" = true ]; then
        echo "Mode: observe only (--no-auto-start)"
    fi
    echo ""

    _watch_status_line() {
        echo -e "\rWatching ${prd_basename} -- last run: ${last_run} -- next: on change"
    }

    _watch_trigger() {
        local change_time
        change_time="$(date '+%H:%M:%S')"

        if [ "$no_auto_start" = true ]; then
            echo "PRD changed: ${change_time}"
            return 0
        fi

        # Kill any currently running loki session
        if [ -n "$_watch_child_pid" ] && kill -0 "$_watch_child_pid" 2>/dev/null; then
            echo "Stopping previous loki session (PID $_watch_child_pid)..."
            kill "$_watch_child_pid" 2>/dev/null
            wait "$_watch_child_pid" 2>/dev/null || true
            _watch_child_pid=""
        fi

        echo ""
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo "PRD changed -- starting loki... (${change_time})"
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo ""

        "$0" start "$prd_path" &
        _watch_child_pid=$!
        last_run="$change_time"
    }

    # Determine watcher backend
    local watcher="poll"
    if command -v fswatch &>/dev/null; then
        watcher="fswatch"
    elif command -v inotifywait &>/dev/null; then
        watcher="inotifywait"
    fi

    echo "Watcher: $watcher"
    _watch_status_line

    case "$watcher" in
        fswatch)
            # Use fswatch with loop-based approach (avoids pipe subshell PID loss)
            while true; do
                fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1 || true
                _watch_trigger
                _watch_status_line
            done
            ;;
        inotifywait)
            while true; do
                inotifywait -qq -e modify "$prd_path" 2>/dev/null
                if [ "$debounce" -gt 0 ]; then
                    sleep "$debounce"
                fi
                _watch_trigger
                _watch_status_line
            done
            ;;
        poll)
            # Universal fallback: stat-based polling
            local last_mtime
            last_mtime=$(stat -f '%m' "$prd_path" 2>/dev/null || stat -c '%Y' "$prd_path" 2>/dev/null || echo "0")
            local debounce_pending=false
            local debounce_deadline=0

            while true; do
                sleep "$poll_interval"
                local current_mtime
                current_mtime=$(stat -f '%m' "$prd_path" 2>/dev/null || stat -c '%Y' "$prd_path" 2>/dev/null || echo "0")

                if [ "$current_mtime" != "$last_mtime" ]; then
                    last_mtime="$current_mtime"
                    if [ "$debounce" -gt 0 ]; then
                        # Start debounce window
                        debounce_pending=true
                        debounce_deadline=$(( $(date +%s) + debounce ))
                    else
                        _watch_trigger
                        _watch_status_line
                    fi
                fi

                # Check debounce deadline
                if [ "$debounce_pending" = true ] && [ "$(date +%s)" -ge "$debounce_deadline" ]; then
                    debounce_pending=false
                    _watch_trigger
                    _watch_status_line
                fi
            done
            ;;
    esac
}

#===============================================================================
# loki cockpit - Multi-repo live status as a terminal inline image (v7.128.0)
#
# Renders the Autonomi cockpit frame directly in supported terminals (iTerm2 /
# WezTerm / ghostty via the iTerm2 protocol; Kitty via its graphics protocol).
# On any unsupported terminal, or when rasterization is unavailable, it falls
# back HONESTLY to a compact text summary and points at the browser dashboard.
# This never touches `loki watch` (the PRD-file auto-rerun watcher).
#===============================================================================

# Compact honest text summary printed on the fallback path. Reads the same
# CockpitState JSON the renderer would have used, so text and image agree.
_cockpit_text_summary() {
    local skill_dir="$1" focus_repo="$2"
    local state_json
    state_json="$(cockpit_gather_state "$skill_dir" "$focus_repo" 2>/dev/null || true)"
    if [ -z "$state_json" ] || ! command -v python3 >/dev/null 2>&1; then
        echo "  (no cockpit state available)"
        return 0
    fi
    LOKI_CK_STATE="$state_json" python3 - <<'PYTXT' 2>/dev/null || echo "  (could not render text summary)"
import json, os
s = json.loads(os.environ.get("LOKI_CK_STATE") or "{}")
print(f"  Run       {s.get('run','?')}")
print(f"  Iteration {s.get('iteration',0)}   Phase {s.get('phase','?')}   Verdict {str(s.get('verdict','?')).upper()}")
tier = s.get('tier') or '-'
prov = s.get('provider') or '-'
print(f"  Tier {tier}   Provider {prov}   Budget ${float(s.get('budgetUsd',0)):.2f}")
gates = s.get('gates') or []
if gates:
    line = "  Gates     " + "  ".join(f"{g.get('name','?')}={g.get('status','?')}" for g in gates[:8])
    print(line)
council = s.get('council') or []
if council:
    print("  Council   " + "  ".join(f"{c.get('reviewer','?')}:{c.get('vote','?')}" for c in council[:6]))
fleet = s.get('fleet') or []
if fleet:
    cap = 8  # cap so a huge registry (e.g. 171 runs) never floods the summary
    active = sum(1 for r in fleet if r.get("running"))
    # Header shows active vs total so a long tail of finished/stale runs reads
    # honestly (e.g. "0 active / 136 total") instead of an alarming bare count.
    if active:
        print(f"  Fleet ({active} active / {len(fleet)} total):")
    else:
        print(f"  Fleet ({len(fleet)} total, none active):")
    for r in fleet[:cap]:
        mark = "*" if r.get("running") else " "
        print(f"    {mark} {r.get('name','?'):<28} iter {r.get('iteration',0)}  {r.get('phase') or r.get('status') or ''}")
    if len(fleet) > cap:
        print(f"    + {len(fleet) - cap} more")
PYTXT
}

cmd_cockpit() {
    local protocol="auto"
    local no_image="0"
    local run_once="0"
    local follow="1"
    local interval="2"
    local focus_repo=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki cockpit${NC} - Live multi-repo status as a terminal inline image"
                echo ""
                echo "Usage: loki cockpit [options]"
                echo ""
                echo "Renders the Autonomi cockpit (iteration, phase, tier, provider, gates,"
                echo "council votes, verdict, budget, and the multi-repo fleet) as an inline"
                echo "image in supported terminals. Falls back to a text summary + the browser"
                echo "dashboard on terminals without inline-image support."
                echo ""
                echo "Options:"
                echo "  --once                 Render one frame then exit"
                echo "  --follow               Re-render on an interval (default)"
                echo "  --interval N           Seconds between frames in --follow (default: 2)"
                echo "  --repo PATH            Focus one repo instead of the running fleet head"
                echo "  --protocol P           iterm2 | kitty | auto (default: auto-detect)"
                echo "  --no-image             Force the text/browser fallback"
                echo "  --help, -h             Show this help"
                echo ""
                echo "Supported terminals: iTerm2, WezTerm, ghostty (iTerm2 protocol); Kitty."
                echo "Override detection with LOKI_COCKPIT_PROTOCOL=iterm2|kitty."
                echo ""
                echo "Note: this is distinct from 'loki watch', which auto-reruns on PRD edits."
                return 0
                ;;
            --once) run_once="1"; follow="0"; shift ;;
            --follow) follow="1"; run_once="0"; shift ;;
            --interval) interval="${2:-2}"; [ $# -ge 2 ] && shift 2 || shift ;;
            --interval=*) interval="${1#*=}"; shift ;;
            --repo) focus_repo="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
            --repo=*) focus_repo="${1#*=}"; shift ;;
            --protocol) protocol="${2:-auto}"; [ $# -ge 2 ] && shift 2 || shift ;;
            --protocol=*) protocol="${1#*=}"; shift ;;
            --no-image) no_image="1"; shift ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki cockpit --help' for usage."
                return 1
                ;;
            *)
                if [ -z "$focus_repo" ]; then
                    focus_repo="$1"
                else
                    echo -e "${RED}Unexpected argument: $1${NC}"
                    return 1
                fi
                shift
                ;;
        esac
    done

    # Load the render/gather helpers (pure logic lives in loki-ts).
    local ts_dir="${_LOKI_SCRIPT_DIR}/../loki-ts"
    local render_lib="${_LOKI_SCRIPT_DIR}/lib/cockpit-render.sh"
    if [ -f "$render_lib" ]; then
        # shellcheck disable=SC1090
        source "$render_lib"
    else
        echo -e "${RED}cockpit render library not found${NC} ($render_lib)" >&2
        return 1
    fi

    _cockpit_frame() {
        local rc=0
        # cockpit_render prints the terminal image to stdout on success (rc 0),
        # or exits 3 (fallback) / 2 (bun unavailable). Capture stderr for the
        # FALLBACK reason without swallowing the image bytes on stdout.
        cockpit_render "$SKILL_DIR" "$ts_dir" "$protocol" "$no_image" "$focus_repo" 2>/tmp/loki-cockpit-err.$$ || rc=$?
        if [ "$rc" = "0" ]; then
            rm -f "/tmp/loki-cockpit-err.$$" 2>/dev/null
            return 0
        fi
        # Honest fallback: text summary + browser dashboard pointer. NEVER claim
        # an image was rendered here.
        local reason=""
        if [ -f "/tmp/loki-cockpit-err.$$" ]; then
            reason="$(sed -n 's/^FALLBACK\t//p' "/tmp/loki-cockpit-err.$$" | head -1)"
            rm -f "/tmp/loki-cockpit-err.$$" 2>/dev/null
        fi
        [ -z "$reason" ] && reason="inline image unavailable in this terminal"
        echo ""
        echo -e "${BOLD}Loki Cockpit${NC} ${DIM}(text fallback: ${reason})${NC}"
        _cockpit_text_summary "$SKILL_DIR" "$focus_repo"
        echo ""
        echo -e "  Open the full visual dashboard: ${CYAN}loki dashboard${NC}"
        return 3
    }

    # E3 footer hints. _cockpit_hint is the persistent key legend under a frame;
    # _cockpit_hint_key is shown after an explain/steer panel to say how to return.
    _cockpit_hint() {
        printf '\n'
        echo -e "  ${DIM}Tab/arrows focus  e explain  s steer  q quit${NC}"
    }
    _cockpit_hint_key() {
        printf '\n'
        echo -e "  ${DIM}press any key to return to the live view${NC}"
    }

    # Max mtime (whole seconds) across the focus run's state files -- the honest
    # "something changed" signal for event-driven redraw. Prints empty when stat
    # is unavailable so the caller falls back to a blind interval. Both BSD stat
    # (-f %m, macOS) and GNU stat (-c %Y, Linux) are probed; nothing else is
    # installed. ponytail: whole-second granularity is enough for a status frame.
    _cockpit_mtime() {
        local repo="$1" loki base m best=""
        [ -n "$repo" ] || repo="$(pwd)"
        loki="$repo/.loki"
        for base in autonomy-state.json events.jsonl verify/evidence.json council; do
            local p="$loki/$base"
            [ -e "$p" ] || continue
            m="$(stat -f '%m' "$p" 2>/dev/null || stat -c '%Y' "$p" 2>/dev/null || true)"
            [ -n "$m" ] || continue
            if [ -z "$best" ] || { [ "$m" -gt "$best" ] 2>/dev/null; }; then best="$m"; fi
        done
        printf '%s' "$best"
    }

    if [ "$run_once" = "1" ]; then
        _cockpit_frame
        return 0
    fi

    # --follow (default): re-render on state change until interrupted. Render the
    # FIRST frame BEFORE the alt-screen so a fallback terminal keeps its text
    # summary in the scrollback (looping text is noise -> we stop there).
    local frame_rc=0
    _cockpit_frame || frame_rc=$?
    if [ "$frame_rc" != "0" ]; then
        # Fallback path: no live image loop; the text summary + dashboard hint
        # were already printed once. Point the user at --follow-capable UIs.
        return 0
    fi

    # Image path: enter the alternate screen buffer so redraws never scroll the
    # user's scrollback, and restore the terminal cleanly on any exit. The traps
    # cover Ctrl-C (INT), kill (TERM) and normal return (EXIT). The loop runs in a
    # subshell so the EXIT trap is scoped to the follow loop and cannot leak into
    # the caller's shell.
    #
    # E3 keyboard interactivity: on a TTY we put the terminal in raw single-key
    # mode so Tab/arrows/e/s/q are read without Enter, and the wait between frames
    # becomes a key poll (cockpit_read_key) instead of a blind sleep -- so a key
    # acts instantly instead of after the cadence. Restore stty + cursor on any
    # exit. If stdin is not a TTY, interactivity is skipped and the loop still
    # renders + refreshes exactly as before.
    local interactive=0
    if [ -t 0 ] && [ -t 1 ] && command -v stty >/dev/null 2>&1; then
        interactive=1
    fi
    (
        local _saved_stty=""
        [ "$interactive" = "1" ] && _saved_stty="$(stty -g 2>/dev/null || true)"
        _cockpit_restore() {
            [ -n "$_saved_stty" ] && stty "$_saved_stty" 2>/dev/null || true
            printf '\033[?25h\033[?1049l'   # show cursor, leave alt-screen
        }
        trap '_cockpit_restore; exit 0' INT TERM
        trap '_cockpit_restore' EXIT
        [ "$interactive" = "1" ] && stty -echo -icanon min 0 time 0 2>/dev/null || true
        printf '\033[?1049h\033[?25l'       # enter alt-screen, hide cursor
        printf '\033[H\033[2J'              # home + clear the alt buffer once
        _cockpit_frame
        [ "$interactive" = "1" ] && _cockpit_hint

        # Cadence bounds (seconds): poll every min_cadence for a state change
        # (anti-thrash upper bound on redraw rate); force a refresh at least every
        # max_idle so a stalled run still shows a live freshness clock. --interval
        # sets max_idle (and the blind fallback period when stat is missing).
        local min_cadence=1
        local max_idle="$interval"; [ "$max_idle" -ge 2 ] 2>/dev/null || max_idle=2
        local last_mtime; last_mtime="$(_cockpit_mtime "$focus_repo")"
        local idle=0
        while :; do
            # Wait one cadence step -- via a key poll when interactive (returns
            # early on a keypress), otherwise a plain sleep. On any wait failure
            # (interrupted) break out and let the trap restore the terminal.
            local key=""
            if [ "$interactive" = "1" ]; then
                key="$(cockpit_read_key "$min_cadence" 2>/dev/null || true)"
            else
                sleep "$min_cadence" 2>/dev/null || break
            fi
            idle=$((idle + min_cadence))

            # Dispatch a keypress (no-op if none / not interactive).
            if [ -n "$key" ]; then
                local action; action="$(cockpit_handle_key "$key")"
                case "$action" in
                    quit) break ;;
                    next|prev)
                        local paths; paths="$(cockpit_fleet_paths "$SKILL_DIR")"
                        focus_repo="$(cockpit_cycle_focus "$focus_repo" "$action" "$paths")"
                        last_mtime="$(_cockpit_mtime "$focus_repo")"
                        idle=0
                        printf '\033[H\033[2J'
                        _cockpit_frame || break
                        _cockpit_hint
                        continue ;;
                    explain)
                        printf '\033[H\033[2J'
                        _cockpit_frame
                        cockpit_explain_focus "$focus_repo"
                        _cockpit_hint_key
                        continue ;;
                    steer)
                        printf '\033[H\033[2J'
                        _cockpit_frame
                        cockpit_steer_hint "$focus_repo"
                        _cockpit_hint_key
                        continue ;;
                esac
            fi

            local cur; cur="$(_cockpit_mtime "$focus_repo")"
            local changed=0
            if [ -n "$cur" ]; then
                # Event-driven: redraw when a watched file advanced, or after
                # max_idle seconds with no change (keeps the freshness clock live).
                if [ "$cur" != "$last_mtime" ] || [ "$idle" -ge "$max_idle" ]; then
                    changed=1; last_mtime="$cur"
                fi
            else
                # stat unavailable -> blind interval fallback.
                [ "$idle" -ge "$max_idle" ] && changed=1
            fi
            if [ "$changed" = "1" ]; then
                idle=0
                printf '\033[H\033[2J'   # home + clear alt buffer, then redraw
                _cockpit_frame || break
                [ "$interactive" = "1" ] && _cockpit_hint
            fi
        done
    )
    return 0
}

#===============================================================================
# loki export - Export session data (v6.0.0)
#===============================================================================

cmd_export() {
    local format="${1:---help}"
    local output_path="${2:-}"

    case "$format" in
        --help|-h)
            echo -e "${BOLD}loki export${NC} - Export session data"
            echo ""
            echo "Usage: loki export <format> [output-path]"
            echo ""
            echo "Formats:"
            echo "  json       Full session state as JSON"
            echo "  markdown   Human-readable session summary"
            echo "  csv        Task queue as CSV"
            echo "  timeline   Iteration timeline as JSON"
            echo "  vibe-kanban  Sync .loki/queue tasks into the Vibe Kanban SQLite DB"
            echo ""
            echo "Flags:"
            echo "  --sarif [run-id]   Write .loki/runs/<id>/findings.sarif (SARIF 2.1.0; needs bun)"
            echo ""
            echo "Examples:"
            echo "  loki export json                         # Print to stdout"
            echo "  loki export json session-export.json     # Save to file"
            echo "  loki export markdown                     # Readable summary"
            echo "  loki export csv tasks.csv                # Task queue CSV"
            echo "  loki export timeline                     # Iteration timeline"
            echo "  loki export vibe-kanban                  # Sync tasks to Vibe Kanban"
            exit 0
            ;;
        json)
            require_jq || return 1
            _export_json "$output_path"
            ;;
        markdown|md)
            _export_markdown "$output_path"
            ;;
        csv)
            _export_csv "$output_path"
            ;;
        timeline)
            require_jq || return 1
            _export_timeline "$output_path"
            ;;
        vibe-kanban)
            local vk_script="${SKILL_DIR:-}/scripts/sync-to-vibe-kanban.sh"
            if [ ! -f "$vk_script" ]; then
                echo -e "${RED}Error: sync-to-vibe-kanban.sh not found at $vk_script${NC}" >&2
                return 1
            fi
            bash "$vk_script"
            ;;
        --sarif)
            echo -e "${RED}loki export --sarif needs bun (https://bun.sh).${NC}" >&2
            return 1
            ;;
        *)
            echo -e "${RED}Unknown format: $format${NC}"
            echo "Supported: json, markdown, csv, timeline, vibe-kanban"
            exit 1
            ;;
    esac
}

# R8: Shareable team assets. Bundles a team's invested, reusable assets
# (cross-project + project memory/learnings, the agent registry, PRD
# templates, council config, optionally the R5 wiki) into a portable,
# REDACTED tarball, and re-imports them into another project or fresh clone.
#
# Scope vs cmd_export: cmd_export emits a per-run SESSION SNAPSHOT
# (json/md/csv/timeline) and is NOT portable or redacted. cmd_assets emits a
# portable, redacted TEAM-ASSET tarball -- different scope, different artifact.
# Redaction REUSES the single proof_redact chokepoint via the
# autonomy/lib/assets_bundle.py helper. No second redactor or parallel exporter.
cmd_assets() {
    local subcommand="${1:-help}"
    shift 2>/dev/null || true

    local helper="$_LOKI_SCRIPT_DIR/lib/assets_bundle.py"
    # The help path needs neither the python3 helper nor python3 itself, so
    # guard these preconditions to skip help/empty subcommands. Otherwise
    # `loki assets --help` would error on a missing helper/python3 instead of
    # printing help (same class as #574 / the cmd_monitor fix).
    case "$subcommand" in
        --help|-h|help|"") ;;
        *)
            if [ ! -f "$helper" ]; then
                echo -e "${RED}Error: assets helper not found at $helper${NC}" >&2
                return 1
            fi
            if ! command -v python3 &>/dev/null; then
                echo -e "${RED}Error: python3 is required for 'loki assets'${NC}" >&2
                return 1
            fi
            ;;
    esac

    # Project dir holds .loki/ (memory, council, wiki). Default to cwd.
    local project_dir
    project_dir="$(pwd)"

    # repo_root holds agents/types.json + templates/. The export and import
    # roots are DELIBERATELY ASYMMETRIC (not a bug):
    #   - EXPORT reads from the loki install ($SKILL_DIR), where a team's
    #     edited registry / templates actually live and are read at runtime
    #     (autonomy/loki:9781, 8840 read templates from $SKILL_DIR; cmd_agent
    #     reads agents/types.json from the install first).
    #   - IMPORT writes to the current working directory (the root of the
    #     target clone the user is setting up), NEVER back into the install.
    #     Writing into $SKILL_DIR would clobber the live install's shipped
    #     registry / templates with redacted copies.
    # Honest limitation: agents/templates restore relative to cwd, so run
    # `loki assets import` from the root of the target clone. A global-install
    # user must copy the restored agents/ + templates/ into their install for
    # loki to read them at runtime (memory/learnings/council/wiki are unaffected
    # -- those restore to $HOME/.loki and <project>/.loki and take effect
    # immediately).
    local export_repo_root="${SKILL_DIR:-$(cd "$_LOKI_SCRIPT_DIR/.." && pwd)}"
    local import_repo_root="$project_dir"

    case "$subcommand" in
        --help|-h|help|"")
            echo -e "${BOLD}loki assets${NC} - Export/import shareable team assets (R8)"
            echo ""
            echo "Bundles a team's reusable assets into a portable, redacted"
            echo "tarball so setup compounds into shared, importable value."
            echo ""
            echo "Usage:"
            echo "  loki assets export <bundle.tgz> [--wiki] [--categories a,b,c]"
            echo "  loki assets import <bundle.tgz> [--no-merge] [--into-install]"
            echo "  loki assets inspect <bundle.tgz>"
            echo ""
            echo "Assets bundled (default):"
            echo "  learnings   ~/.loki/learnings/*.jsonl (cross-project)"
            echo "  memory      <project>/.loki/memory/** (episodic/semantic/skills)"
            echo "  agents      agents/types.json (agent registry)"
            echo "  templates   templates/*.md (PRD templates)"
            echo "  council     <project>/.loki/council/*.json (council config/state)"
            echo "  wiki        <project>/.loki/wiki/** (opt-in via --wiki)"
            echo ""
            echo "All bundled content is redacted via the proof_redact chokepoint:"
            echo "secrets, keys, tokens, and absolute home/repo paths are stripped"
            echo "before the bundle is written. Originals on disk are never changed."
            echo ""
            echo "Restore locations on import:"
            echo "  learnings -> \$HOME/.loki/learnings   (effective immediately)"
            echo "  memory    -> <cwd>/.loki/memory       (effective immediately)"
            echo "  council   -> <cwd>/.loki/council      (effective immediately)"
            echo "  wiki      -> <cwd>/.loki/wiki         (effective immediately)"
            echo "  agents    -> <cwd>/agents/types.json"
            echo "  templates -> <cwd>/templates/*.md"
            echo "Run import from the ROOT of the target clone. Note: agents and"
            echo "templates are read by loki from its install dir at runtime. On a"
            echo "global install, pass --into-install so agents/templates restore"
            echo "into the install dir and are read at runtime (default is cwd,"
            echo "which suits a repo-clone where loki runs from the clone root)."
            echo ""
            echo "Examples:"
            echo "  loki assets export team-assets.tgz"
            echo "  loki assets export team-assets.tgz --wiki"
            echo "  loki assets import team-assets.tgz       # merge learnings, overwrite rest"
            echo "  loki assets import team-assets.tgz --no-merge"
            echo "  loki assets inspect team-assets.tgz      # show manifest only"
            return 0
            ;;
        export)
            local out_path="${1:-}"
            if [ -z "$out_path" ]; then
                echo -e "${RED}Usage: loki assets export <bundle.tgz> [--wiki] [--categories a,b,c]${NC}" >&2
                return 1
            fi
            shift 2>/dev/null || true
            if ! _export_check_overwrite "$out_path"; then
                return 1
            fi
            echo "Bundling team assets (redacting secrets and paths)..."
            if LOKI_ASSETS_HOME="$HOME" LOKI_ASSETS_REPO="$export_repo_root" \
               LOKI_ASSETS_PROJECT="$project_dir" \
               python3 "$helper" export "$out_path" "$@"; then
                echo -e "${GREEN}Bundle written: $out_path${NC}"
            else
                echo -e "${RED}Export failed${NC}" >&2
                return 1
            fi
            ;;
        import)
            local bundle_path="${1:-}"
            if [ -z "$bundle_path" ]; then
                echo -e "${RED}Usage: loki assets import <bundle.tgz> [--no-merge] [--into-install]${NC}" >&2
                return 1
            fi
            shift 2>/dev/null || true
            if [ ! -f "$bundle_path" ]; then
                echo -e "${RED}Error: bundle not found: $bundle_path${NC}" >&2
                return 1
            fi
            # --into-install: restore agents/templates into the loki install
            # ($SKILL_DIR) so a global-install user has them read at runtime,
            # instead of into cwd. Strip the flag before passing the rest to
            # the helper (the helper does not understand it). The install dir
            # is the same source export reads from, so this is the symmetric
            # round-trip for a global install. Off by default (cwd is safe).
            local _import_args=()
            local arg
            for arg in "$@"; do
                if [ "$arg" = "--into-install" ]; then
                    import_repo_root="$export_repo_root"
                else
                    _import_args+=("$arg")
                fi
            done
            if [ "$import_repo_root" = "$export_repo_root" ]; then
                echo "Importing team assets (agents/templates -> install dir: $import_repo_root)..."
            else
                echo "Importing team assets into this project/clone (cwd)..."
            fi
            # Bash 3.2 (macOS) errors on "${arr[@]}" when the array is empty
            # and set -u is active; guard with the count.
            if [ "${#_import_args[@]}" -gt 0 ]; then
                LOKI_ASSETS_HOME="$HOME" LOKI_ASSETS_REPO="$import_repo_root" \
                LOKI_ASSETS_PROJECT="$project_dir" \
                python3 "$helper" import "$bundle_path" "${_import_args[@]}"
            else
                LOKI_ASSETS_HOME="$HOME" LOKI_ASSETS_REPO="$import_repo_root" \
                LOKI_ASSETS_PROJECT="$project_dir" \
                python3 "$helper" import "$bundle_path"
            fi
            if [ $? -eq 0 ]; then
                echo -e "${GREEN}Import complete${NC}"
            else
                echo -e "${RED}Import failed${NC}" >&2
                return 1
            fi
            ;;
        inspect)
            local bundle_path="${1:-}"
            if [ -z "$bundle_path" ]; then
                echo -e "${RED}Usage: loki assets inspect <bundle.tgz>${NC}" >&2
                return 1
            fi
            if [ ! -f "$bundle_path" ]; then
                echo -e "${RED}Error: bundle not found: $bundle_path${NC}" >&2
                return 1
            fi
            python3 "$helper" inspect "$bundle_path"
            ;;
        *)
            echo -e "${RED}Unknown subcommand: $subcommand${NC}" >&2
            echo "Run 'loki assets --help' for usage." >&2
            return 1
            ;;
    esac
}

# Guard: check if output file exists and warn before overwriting.
#
# NON-INTERACTIVE SAFETY: this is the single chokepoint every export format
# (_export_json / _export_markdown / _export_csv / _export_timeline) routes
# through, so the guard lives here once rather than in four callers. Without it
# the prompt BLOCKS FOREVER whenever stdin is not closed and not answerable -- a
# CI lane, a pipeline, or a test harness that leaves stdin attached. That wedged
# local-ci for 40+ minutes at tests/cli/test-alias-forwarding.sh (the runner sat
# at 0.0% CPU in state S waiting on a read that could never be satisfied).
#
# Semantics mirror the established precedent at the BMAD prompt (search
# LOKI_AUTO_CONFIRM): LOKI_AUTO_CONFIRM=true (falling back to CI) means ANSWER
# YES, and echoes an auditable "y (auto-confirmed)" line. A non-interactive
# shell WITHOUT that opt-in FAILS CLOSED: it refuses the overwrite and names the
# env var, because silently clobbering a user's file in CI is the worse failure.
# `read` is `|| reply=""` guarded so an EOF cannot abort a `set -e` caller.
_export_check_overwrite() {
    local output="$1"
    if [ -n "$output" ] && [ -f "$output" ]; then
        echo -e "${YELLOW}Warning: File already exists: $output${NC}"
        local _auto="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
        if [ "$_auto" = "true" ]; then
            echo -n "Overwrite? [y/N] "
            echo "y (auto-confirmed)"
            return 0
        fi
        # A prompt is only answerable when BOTH ends are a terminal. Testing
        # stdin alone was the bug: under a PTY harness that redirects stdout
        # (the gate runs every check as `bash tests/... | tail -3`), `-t 0` is
        # true, so we fell through to `read` -- but the process is not the
        # terminal's foreground group, so reading the tty raises SIGTTIN and
        # STOPS it. A stopped process never runs its SIGALRM handler, which is
        # why `read -t` cannot rescue this; only not reading at all can.
        if [ ! -t 0 ] || [ ! -t 1 ]; then
            echo "Export cancelled (non-interactive: refusing to overwrite '$output'." >&2
            echo "Set LOKI_AUTO_CONFIRM=true to overwrite, or choose a new output path.)" >&2
            return 1
        fi
        echo -n "Overwrite? [y/N] "
        local reply
        read -r reply || reply=""
        case "$reply" in
            [yY]|[yY][eE][sS]) return 0 ;;
            *) echo "Export cancelled (set LOKI_AUTO_CONFIRM=true to overwrite)."; return 1 ;;
        esac
    fi
    return 0
}

_export_json() {
    local output="$1"

    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${RED}No active session found.${NC}"
        exit 1
    fi

    local json_output
    json_output=$(LOKI_DIR="$LOKI_DIR" LOKI_VERSION="$(get_version)" python3 << 'EXPORT_JSON'
import json, os, glob
from datetime import datetime

loki_dir = os.environ.get("LOKI_DIR", ".loki")
export = {
    "exported_at": datetime.utcnow().isoformat() + "Z",
    "version": os.environ.get("LOKI_VERSION", "0.0.0"),
    "session": {},
    "queue": {},
    "quality": {},
    "config": {}
}

# Session state
for state_file in glob.glob(os.path.join(loki_dir, "state", "*")):
    name = os.path.basename(state_file)
    try:
        with open(state_file) as f:
            content = f.read().strip()
        try:
            export["session"][name] = json.loads(content)
        except json.JSONDecodeError:
            export["session"][name] = content
    except:
        pass

# Queue
for queue in ["pending", "in-progress", "completed", "failed"]:
    qf = os.path.join(loki_dir, "queue", f"{queue}.json")
    if os.path.exists(qf):
        try:
            with open(qf) as f:
                export["queue"][queue] = json.load(f)
        except:
            pass

# Quality reviews (latest 5)
reviews_dir = os.path.join(loki_dir, "quality", "reviews")
if os.path.isdir(reviews_dir):
    reviews = sorted(os.listdir(reviews_dir), reverse=True)[:5]
    export["quality"]["recent_reviews"] = reviews

# Config
cfg = os.path.join(loki_dir, "config", "settings.json")
if os.path.exists(cfg):
    try:
        with open(cfg) as f:
            export["config"] = json.load(f)
    except:
        pass

print(json.dumps(export, indent=2, default=str))
EXPORT_JSON
    )

    if [ -n "$output" ]; then
        # Reject path traversal
        if [[ "$output" == *".."* ]]; then
            echo -e "${RED}Error: Output path must not contain '..'${NC}"
            return 1
        fi
        _export_check_overwrite "$output" || return 0
        echo "$json_output" > "$output"
        echo -e "${GREEN}Exported to $output${NC}"
    else
        echo "$json_output"
    fi
}

_export_markdown() {
    local output="$1"

    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${RED}No active session found.${NC}"
        exit 1
    fi

    local md_output
    md_output=$(cat << MDEOF
# Loki Mode Session Export

**Exported:** $(date -u +%Y-%m-%dT%H:%M:%SZ)
**Directory:** $(pwd)

## Status

$(cat "$LOKI_DIR/STATUS.txt" 2>/dev/null || echo "No status file")

## Recent Commits

$(git log --oneline -10 2>/dev/null || echo "No git history")

## Task Queue Summary

$(for q in pending in-progress completed failed; do
    f="$LOKI_DIR/queue/${q}.json"
    if [ -f "$f" ]; then
        count=$(_QUEUE_FILE="$f" python3 -c "import json, os; print(len(json.load(open(os.environ['_QUEUE_FILE']))))" 2>/dev/null || echo "0")
        echo "- **$q:** $count tasks"
    fi
done)
MDEOF
    )

    if [ -n "$output" ]; then
        if [[ "$output" == *".."* ]]; then
            echo -e "${RED}Error: Output path must not contain '..'${NC}"
            return 1
        fi
        _export_check_overwrite "$output" || return 0
        echo "$md_output" > "$output"
        echo -e "${GREEN}Exported to $output${NC}"
    else
        echo "$md_output"
    fi
}

_export_csv() {
    local output="$1"

    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${RED}No active session found.${NC}"
        exit 1
    fi

    local csv_output
    csv_output=$(LOKI_DIR="$LOKI_DIR" python3 << 'EXPORT_CSV'
import json, os, csv, io

loki_dir = os.environ.get("LOKI_DIR", ".loki")
writer_buf = io.StringIO()
writer = csv.writer(writer_buf)
writer.writerow(["status", "id", "title", "created_at"])

for queue in ["pending", "in-progress", "completed", "failed"]:
    qf = os.path.join(loki_dir, "queue", f"{queue}.json")
    if os.path.exists(qf):
        try:
            with open(qf) as f:
                data = json.load(f)
            tasks = data.get("tasks", data) if isinstance(data, dict) else data
            for t in tasks:
                writer.writerow([
                    queue,
                    t.get("id", ""),
                    t.get("title", t.get("task", "")),
                    t.get("created_at", "")
                ])
        except:
            pass

print(writer_buf.getvalue())
EXPORT_CSV
    )

    if [ -n "$output" ]; then
        if [[ "$output" == *".."* ]]; then
            echo -e "${RED}Error: Output path must not contain '..'${NC}"
            return 1
        fi
        _export_check_overwrite "$output" || return 0
        echo "$csv_output" > "$output"
        echo -e "${GREEN}Exported to $output${NC}"
    else
        echo "$csv_output"
    fi
}

_export_timeline() {
    local output="$1"

    if [ ! -d "$LOKI_DIR" ]; then
        echo -e "${RED}No active session found.${NC}"
        exit 1
    fi

    local timeline_output
    timeline_output=$(python3 << 'EXPORT_TIMELINE'
import json, os, glob

loki_dir = os.environ.get("LOKI_DIR", ".loki")
timeline = []

# Collect iteration logs
log_dir = os.path.join(loki_dir, "logs")
if os.path.isdir(log_dir):
    for f in sorted(glob.glob(os.path.join(log_dir, "iteration-*.json"))):
        try:
            with open(f) as fh:
                timeline.append(json.load(fh))
        except:
            pass

# Collect council verdicts
council_dir = os.path.join(loki_dir, "council")
if os.path.isdir(council_dir):
    for f in sorted(glob.glob(os.path.join(council_dir, "votes", "iteration-*", "aggregate.json"))):
        try:
            with open(f) as fh:
                data = json.load(fh)
                data["_type"] = "council_vote"
                timeline.append(data)
        except:
            pass

print(json.dumps(timeline, indent=2, default=str))
EXPORT_TIMELINE
    )

    if [ -n "$output" ]; then
        if [[ "$output" == *".."* ]]; then
            echo -e "${RED}Error: Output path must not contain '..'${NC}"
            return 1
        fi
        _export_check_overwrite "$output" || return 0
        echo "$timeline_output" > "$output"
        echo -e "${GREEN}Exported to $output${NC}"
    else
        echo "$timeline_output"
    fi
}

cmd_config() {
    local subcommand="${1:-show}"
    shift 2>/dev/null || true

    case "$subcommand" in
        show)
            cmd_config_show
            ;;
        list)
            # HELP slice: `config list [--json]` is `config show` (json: wrapped).
            # shellcheck source=/dev/null
            source "$_LOKI_SCRIPT_DIR/lib/json-output.sh"
            if loki_json_requested "$@"; then
                loki_json_wrap config-list cmd_config_show
            else
                cmd_config_show
            fi
            ;;
        init)
            cmd_config_init
            ;;
        edit)
            cmd_config_edit
            ;;
        path)
            cmd_config_path
            ;;
        set)
            cmd_config_set "$@"
            ;;
        get)
            cmd_config_get "$@"
            ;;
        example)
            cmd_config_example "$@"
            ;;
        schema)
            cmd_config_schema "$@"
            ;;
        validate)
            cmd_config_validate "$@"
            ;;
        help|--help|-h)
            echo -e "${YELLOW}Usage: loki config [show|list|init|edit|path|set|get|example|schema|validate]${NC}"
            echo ""
            echo "  show            Show current configuration (default)"
            echo "  list [--json]   Same as show; --json emits one JSON object"
            echo "  init            Create a config file from template"
            echo "  edit            Open config file in editor"
            echo "  path            Show config file paths"
            echo "  set KEY VALUE   Set a configuration value"
            echo "  get KEY         Get a configuration value"
            echo "  example         Print an annotated config template (for --config)"
            echo "  schema          Print the key -> env-var mapping table"
            echo "  validate [FILE] Validate loki.yaml (no FILE: repo root, then ~/.loki) or a --config file"
            echo ""
            echo "Settable keys:"
            echo "  maxTier              Cost ceiling: opus, sonnet, haiku (default: opus)"
            echo "  model.planning       Model for planning tier"
            echo "  model.development    Model for development tier"
            echo "  model.fast           Model for fast tier"
            echo "  provider             Default AI provider: claude, codex, cline, aider, opencode"
            echo "  issue.provider       Default issue provider: github, gitlab, jira, azure_devops"
            echo "  blind_validation     Blind validation mode: true, false (default: true)"
            echo "  adversarial_testing  Adversarial testing: true, false (default: true)"
            echo "  log_level            Output verbosity: debug, info, warn, error (default: info)"
            echo "  spawn_timeout        [DEPRECATED] No effect since WAVE9 (no consumer); accepted for back-compat"
            echo "  spawn_retries        [DEPRECATED] No effect since WAVE9 (no consumer); accepted for back-compat"
            echo "  notify.slack         Slack webhook URL"
            echo "  notify.discord       Discord webhook URL"
            echo "  budget               Cost budget limit in USD"
            ;;
        *)
            echo "loki config: unknown subcommand '${subcommand}' (see 'loki config help')" >&2
            return 2
            ;;
    esac
}

# v6.0.0: Set a configuration value
cmd_config_set() {
    local use_global=false

    # Check for --global flag
    if [[ "${1:-}" == "--global" ]]; then
        use_global=true
        shift
    fi

    local key="${1:-}"
    local value="${2:-}"

    if [[ -z "$key" || -z "$value" ]]; then
        echo -e "${RED}Usage: loki config set [--global] <key> <value>${NC}"
        echo "Run 'loki config' for list of settable keys."
        return 2
    fi

    # Determine config directory: --global writes to ~/.config/loki-mode/
    local config_dir
    if $use_global; then
        config_dir="${HOME}/.config/loki-mode"
    else
        config_dir="$LOKI_DIR/config"
    fi

    # Ensure config directory exists
    mkdir -p "$config_dir"
    local config_store="$config_dir/settings.json"

    # Initialize if not exists
    if [ ! -f "$config_store" ]; then
        echo '{}' > "$config_store"
    fi

    # Validate known keys and values
    case "$key" in
        maxTier)
            case "$value" in
                opus|sonnet|haiku) ;;
                *) echo -e "${RED}Invalid maxTier: $value (expected: opus, sonnet, haiku)${NC}"; return 1 ;;
            esac
            ;;
        provider)
            case "$value" in
                gemini)
                    echo -e "${RED}Invalid provider: 'gemini' is deprecated as of v7.5.18. Active providers: claude, codex, cline, aider, opencode${NC}"
                    return 1
                    ;;
                claude|codex|cline|aider|opencode) ;;
                *) echo -e "${RED}Invalid provider: $value (expected: claude, codex, cline, aider, opencode)${NC}"; return 1 ;;
            esac
            ;;
        issue.provider)
            case "$value" in
                github|gitlab|jira|azure_devops) ;;
                *) echo -e "${RED}Invalid issue.provider: $value${NC}"; return 1 ;;
            esac
            ;;
        blind_validation|adversarial_testing)
            case "$value" in
                true|false) ;;
                *) echo -e "${RED}Invalid $key: $value (expected: true, false)${NC}"; return 1 ;;
            esac
            ;;
        log_level)
            case "$value" in
                debug|info|warn|error) ;;
                *) echo -e "${RED}Invalid log_level: $value (expected: debug, info, warn, error)${NC}"; return 1 ;;
            esac
            ;;
        spawn_timeout|spawn_retries)
            if ! echo "$value" | grep -qE '^[0-9]+$'; then
                echo -e "${RED}Invalid $key: $value (expected: integer)${NC}"; return 1
            fi
            echo -e "${YELLOW}Note: '$key' is deprecated and has no effect since WAVE9 (no consumer in the runtime). Accepted for back-compat only.${NC}" >&2
            ;;
        budget)
            if ! echo "$value" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
                echo -e "${RED}Invalid budget: $value (expected: positive numeric USD amount)${NC}"; return 1
            fi
            # Reject zero and negative values (#68)
            if echo "$value" | grep -qE '^0+(\.0+)?$'; then
                echo -e "${RED}Invalid budget: $value (must be greater than 0)${NC}"; return 1
            fi
            ;;
        model.planning|model.development|model.fast)
            # Validate model names: alphanumeric, dots, hyphens, underscores only
            if ! echo "$value" | grep -qE '^[a-zA-Z0-9._-]+$'; then
                echo -e "${RED}Invalid model name: $value (only alphanumeric, dots, hyphens, underscores)${NC}"; return 1
            fi
            ;;
        notify.slack|notify.discord)
            # Validate webhook URLs: must be https
            if ! echo "$value" | grep -qiE '^https://'; then
                echo -e "${RED}Invalid webhook URL: must start with https://${NC}"; return 1
            fi
            ;;
        *)
            echo -e "${RED}Unknown configuration key: '$key'${NC}"
            echo ""
            echo "Valid keys: maxTier, provider, issue.provider, blind_validation,"
            echo "  adversarial_testing, log_level, spawn_timeout, spawn_retries, budget,"
            echo "  model.planning, model.development, model.fast,"
            echo "  notify.slack, notify.discord"
            echo ""
            echo "Run 'loki config' for details on each key."
            return 1
            ;;
    esac

    # Update JSON config via python (use inline env vars to avoid leaking into environment)
    LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" LOKI_CFG_VALUE="$value" \
    python3 << 'SET_CONFIG'
import json, os
cfg_file = os.environ["LOKI_CFG_FILE"]
key = os.environ["LOKI_CFG_KEY"]
value = os.environ["LOKI_CFG_VALUE"]

with open(cfg_file) as f:
    config = json.load(f)

# Handle dotted keys (model.planning -> {"model": {"planning": value}})
parts = key.split(".")
current = config
for part in parts[:-1]:
    if part not in current or not isinstance(current[part], dict):
        current[part] = {}
    current = current[part]
# Type coercion: try int, then float, then bool, else string
def coerce_type(v):
    if v.lower() in ('true', 'false'):
        return v.lower() == 'true'
    try:
        return int(v)
    except ValueError:
        pass
    try:
        return float(v)
    except ValueError:
        pass
    return v

current[parts[-1]] = coerce_type(value)

with open(cfg_file, "w") as f:
    json.dump(config, f, indent=2)
SET_CONFIG

    echo -e "${GREEN}Set $key = $value${NC}"

    # Also set env var for immediate effect in current session
    case "$key" in
        maxTier)          export LOKI_MAX_TIER="$value" ;;
        provider)         export LOKI_PROVIDER="$value" ;;
        blind_validation) export LOKI_BLIND_VALIDATION="$value" ;;
        adversarial_testing) export LOKI_ADVERSARIAL_TESTING="$value" ;;
        # spawn_timeout / spawn_retries: deprecated, no runtime consumer (WAVE9);
        # intentionally not exported.
        budget)           export LOKI_BUDGET_LIMIT="$value" ;;
    esac
}

# #691: print an annotated config template generated from LOKI_CONFIG_MAP, so it
# can never drift from the parsers. Suitable for `loki start --config <path>`.
cmd_config_example() {
    if ! declare -f loki_config_generate_example >/dev/null 2>&1; then
        echo -e "${RED}config example: config-map.sh helper not loaded${NC}" >&2
        return 1
    fi
    loki_config_generate_example
}

# #691: print the machine-readable key -> LOKI_ENV_VAR mapping table.
cmd_config_schema() {
    if ! declare -f loki_config_generate_schema >/dev/null 2>&1; then
        echo -e "${RED}config schema: config-map.sh helper not loaded${NC}" >&2
        return 1
    fi
    loki_config_generate_schema
}

# multi-repo workspaces (D51 Phase B). On by default; LOKI_WORKSPACES=0 disables.
cmd_workspace() {
    if [ "${LOKI_WORKSPACES:-}" = "0" ]; then
        echo -e "${RED}loki workspace is disabled.${NC} Unset LOKI_WORKSPACES (it is set to 0)." >&2
        return 2
    fi
    command -v python3 >/dev/null 2>&1 || { echo -e "${RED}loki workspace: python3 is required${NC}" >&2; return 2; }
    python3 "$_LOKI_SCRIPT_DIR/lib/workspace.py" "$@"
}

# headless backlog runner. One v10 issue-mode run per open issue, each in
# its own worktree and branch, at most N in parallel (loki.yaml concurrency).
cmd_backlog() {
    case "${1:-}" in
        ""|--help|-h|help)
            echo -e "${BOLD}loki backlog${NC} - run a v10 issue-mode run for every matching open issue"
            echo ""
            echo "Usage: loki backlog owner/repo (--all | --label X | --issues 1,2,3) [--concurrency N] [--dry-run]"
            echo ""
            echo "  --all             Every open issue"
            echo "  --label X         Open issues with label X"
            echo "  --issues 1,2,3    Exactly these issue numbers"
            echo "  --concurrency N   Max runs in parallel (default: loki.yaml concurrency, else 2)"
            echo "  --dry-run         List what would run; launch nothing"
            echo ""
            echo "Run from a checkout of owner/repo. Each issue runs in its own worktree on loki/backlog-N."
            echo "Config: loki.yaml (repo root, then ~/.loki/loki.yaml); see docs/loki.yaml.example."
            echo "Exit: 0 every issue ended VERIFIED or with a PR; 3 only budget stops; 1 otherwise; 2 usage."
            return 0
            ;;
    esac
    command -v python3 >/dev/null 2>&1 || { echo -e "${RED}loki backlog: python3 is required${NC}" >&2; return 2; }
    python3 "$_LOKI_SCRIPT_DIR/lib/backlog.py" "$@"
}

# #691: validate a config file (--config) WITHOUT applying it. Reports
# unresolved ${VAR} refs, raw-secret literals (error), and per-value validation
# failures. Non-zero exit on any failure.
cmd_config_validate() {
    local file="${1:-}" _loki_yaml
    # loki.yaml (UI + headless config) has its own schema; no argument
    # validates the discovered loki.yaml (repo root, then ~/.loki/loki.yaml).
    case "$(basename -- "$file")" in "" | loki.yaml | loki.yml | loki.yaml.example) _loki_yaml=1 ;; *) _loki_yaml=0 ;; esac
    if [ "$_loki_yaml" = 1 ]; then
        command -v python3 >/dev/null 2>&1 || { echo -e "${RED}config validate: python3 is required${NC}" >&2; return 2; }
        python3 "$_LOKI_SCRIPT_DIR/lib/loki_yaml.py" validate ${file:+"$file"}
        return $?
    fi
    if ! declare -f loki_config_validate_file >/dev/null 2>&1; then
        echo -e "${RED}config validate: config-map.sh helper not loaded${NC}" >&2
        return 1
    fi
    loki_config_validate_file "$file"
}

# v6.0.0: Get a configuration value
cmd_config_get() {
    local key="${1:-}"

    if [[ -z "$key" ]]; then
        echo -e "${RED}Usage: loki config get <key>${NC}"
        return 2
    fi

    local config_store="$LOKI_DIR/config/settings.json"
    if [ ! -f "$config_store" ]; then
        echo -e "${YELLOW}No config found (using defaults)${NC}"
        return 0
    fi

    local result
    result=$(LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" \
    python3 << 'GET_CONFIG'
import json, os
cfg_file = os.environ["LOKI_CFG_FILE"]
key = os.environ["LOKI_CFG_KEY"]

with open(cfg_file) as f:
    config = json.load(f)

parts = key.split(".")
current = config
for part in parts:
    if isinstance(current, dict) and part in current:
        current = current[part]
    else:
        print("")
        exit(0)

print(current if not isinstance(current, dict) else json.dumps(current, indent=2))
GET_CONFIG
    ) 2>/dev/null || {
        echo -e "${RED}Error reading config key '$key'${NC}"
        return 1
    }
    echo "$result"
}

cmd_config_show() {
    echo -e "${BOLD}Loki Mode Configuration${NC}"
    echo ""
    echo -e "${CYAN}Installation:${NC} $SKILL_DIR"
    echo -e "${CYAN}Version:${NC} $(get_version)"
    echo ""

    # Check for config files
    local config_file=""
    if [ -f ".loki/config.yaml" ]; then
        config_file=".loki/config.yaml"
        echo -e "${GREEN}Config file:${NC} $config_file (project-local)"
    elif [ -f ".loki/config.yml" ]; then
        config_file=".loki/config.yml"
        echo -e "${GREEN}Config file:${NC} $config_file (project-local)"
    elif [ -f "${HOME}/.config/loki-mode/config.yaml" ]; then
        config_file="${HOME}/.config/loki-mode/config.yaml"
        echo -e "${GREEN}Config file:${NC} $config_file (user-global)"
    elif [ -f "${HOME}/.config/loki-mode/config.yml" ]; then
        config_file="${HOME}/.config/loki-mode/config.yml"
        echo -e "${GREEN}Config file:${NC} $config_file (user-global)"
    else
        echo -e "${YELLOW}Config file:${NC} Not found (using defaults)"
    fi
    echo ""

    echo -e "${CYAN}Current Settings:${NC}"
    echo ""
    echo "Core:"
    echo "  max_retries:      ${LOKI_MAX_RETRIES:-50}"
    echo "  base_wait:        ${LOKI_BASE_WAIT:-60}s"
    echo "  max_wait:         ${LOKI_MAX_WAIT:-3600}s"
    echo ""
    echo "Dashboard:"
    echo "  enabled:          ${LOKI_DASHBOARD:-true}"
    echo "  port:             ${LOKI_DASHBOARD_PORT:-57374}"
    echo ""
    echo "Notifications:"
    echo "  enabled:          ${LOKI_NOTIFICATIONS:-true}"
    echo "  sound:            ${LOKI_NOTIFICATION_SOUND:-true}"
    echo ""
    echo "GitHub Integration:"
    echo "  import:           ${LOKI_GITHUB_IMPORT:-false}"
    echo "  pr:               ${LOKI_GITHUB_PR:-false}"
    echo "  sync:             ${LOKI_GITHUB_SYNC:-false}"
    echo ""
    echo "Provider:"
    echo "  provider:         ${LOKI_PROVIDER:-claude}"
    echo ""
    echo "Execution:"
    echo "  complexity:       ${LOKI_COMPLEXITY:-auto}"
    echo "  parallel_mode:    ${LOKI_PARALLEL_MODE:-false}"
    echo "  max_iterations:   ${LOKI_MAX_ITERATIONS:-1000}"
    echo "  autonomy_mode:    ${LOKI_AUTONOMY_MODE:-perpetual}"
    echo ""
    echo "Settings:"
    echo "  maxTier:              ${LOKI_MAX_TIER:-(unlimited)}"
    echo "  blind_validation:     ${LOKI_BLIND_VALIDATION:-true}"
    echo "  adversarial_testing:  ${LOKI_ADVERSARIAL_TESTING:-true}"
    echo "  spawn_timeout:        (deprecated, no effect)"
    echo "  spawn_retries:        (deprecated, no effect)"
    echo "  issue_provider:       ${LOKI_ISSUE_PROVIDER:-(auto-detect)}"
    echo "  budget:               ${LOKI_BUDGET_LIMIT:-(no limit)}"
    echo ""
    echo -e "Run ${CYAN}loki config path${NC} to see all config file locations"
    echo -e "Run ${CYAN}loki config set <key> <value>${NC} to change a setting"
}

cmd_config_init() {
    local template="$SKILL_DIR/autonomy/config.example.yaml"
    local target=".loki/config.yaml"
    local global_target="${HOME}/.config/loki-mode/config.yaml"

    if [ ! -f "$template" ]; then
        echo -e "${RED}Error: Config template not found at $template${NC}"
        exit 1
    fi

    echo -e "${BOLD}Initialize Configuration${NC}"
    echo ""
    echo "Where do you want to create the config file?"
    echo ""
    echo "  1) $target (project-local, recommended)"
    echo "  2) $global_target (user-global)"
    echo ""
    read -p "Choice [1]: " choice
    choice="${choice:-1}"

    case "$choice" in
        1)
            mkdir -p ".loki"
            cp "$template" "$target"
            echo -e "${GREEN}Created: $target${NC}"
            ;;
        2)
            mkdir -p "${HOME}/.config/loki-mode"
            cp "$template" "$global_target"
            echo -e "${GREEN}Created: $global_target${NC}"
            ;;
        *)
            echo -e "${RED}Invalid choice${NC}"
            exit 1
            ;;
    esac

    echo ""
    echo "Edit with: loki config edit"
}

cmd_config_edit() {
    local config_file=""

    # Find existing config file
    if [ -f ".loki/config.yaml" ]; then
        config_file=".loki/config.yaml"
    elif [ -f ".loki/config.yml" ]; then
        config_file=".loki/config.yml"
    elif [ -f "${HOME}/.config/loki-mode/config.yaml" ]; then
        config_file="${HOME}/.config/loki-mode/config.yaml"
    elif [ -f "${HOME}/.config/loki-mode/config.yml" ]; then
        config_file="${HOME}/.config/loki-mode/config.yml"
    fi

    if [ -z "$config_file" ]; then
        echo -e "${YELLOW}No config file found.${NC}"
        # Non-interactive safety (same class as _export_check_overwrite): a bare
        # `read` here blocks forever when stdin is attached but unanswerable.
        # This prompt DEFAULTS TO YES, so auto-confirm and non-interactive both
        # take the create path; only the explicit interactive "n" declines.
        local create=""
        local _auto="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
        if [ "$_auto" = "true" ] || [ ! -t 0 ]; then
            echo "Create one? [Y/n]: Y (non-interactive default; run 'loki config init' to control this)"
            create="Y"
        else
            read -r -p "Create one? [Y/n]: " create || create=""
        fi
        create="${create:-Y}"
        if [[ "$create" =~ ^[Yy] ]]; then
            cmd_config_init
            config_file=".loki/config.yaml"
        else
            exit 0
        fi
    fi

    # Open in editor
    local editor="${EDITOR:-${VISUAL:-vim}}"
    echo -e "${GREEN}Opening $config_file with $editor${NC}"
    "$editor" "$config_file"
}

cmd_config_path() {
    echo -e "${BOLD}Config File Search Paths${NC}"
    echo ""
    echo "Loki Mode searches for config files in this order:"
    echo ""

    local paths=(
        ".loki/config.yaml"
        ".loki/config.yml"
        "${HOME}/.config/loki-mode/config.yaml"
        "${HOME}/.config/loki-mode/config.yml"
    )

    for path in "${paths[@]}"; do
        if [ -f "$path" ]; then
            echo -e "  ${GREEN}[FOUND]${NC}  $path"
        else
            echo -e "  ${YELLOW}[-----]${NC}  $path"
        fi
    done

    echo ""
    echo "Template: $SKILL_DIR/autonomy/config.example.yaml"
    echo ""
    echo "Create a config file with: loki config init"
}

# Merge the Loki plugin into a Claude Code settings.json (loki setup-skill --claude-code).
# Existing keys are never overwritten, invalid JSON is refused (rc 3), writes are atomic.
_setup_skill_claude_code() {
    local print_only=0 project=0 stop=0 settings=""
    while [ $# -gt 0 ]; do
        case "$1" in
            --print) print_only=1 ;;
            --project) project=1 ;;
            --stop-hook) stop=1 ;;
            --settings) settings="${2:-}"; shift ;;
            --help|-h)
                echo "Usage: loki setup-skill --claude-code [--print] [--project] [--stop-hook] [--settings FILE]"
                echo ""
                echo "Registers the Loki plugin marketplace and enables the plugin in Claude Code"
                echo "settings (default ~/.claude/settings.json; --project uses ./.claude/settings.json)."
                echo "Merges only: existing keys are kept, invalid JSON is refused."
                echo ""
                echo "  --print       Print the settings snippet and change nothing"
                echo "  --stop-hook   Also opt in to the Stop-hook fast verifier (LOKI_STOP_VERIFY=1)"
                return 0
                ;;
            *) echo "Unknown option: $1" >&2; return 2 ;;
        esac
        shift
    done
    command -v python3 >/dev/null 2>&1 || { echo "python3 is required" >&2; return 1; }
    if [ -z "$settings" ] && [ "$print_only" -eq 0 ]; then
        if [ "$project" -eq 1 ]; then
            settings="${TARGET_DIR:-.}/.claude/settings.json"
        else
            settings="$HOME/.claude/settings.json"
        fi
    fi
    local out rc
    out="$(python3 - "$settings" "$stop" "$print_only" <<'PY'
import json, os, sys, tempfile
path, stop, print_only = sys.argv[1], sys.argv[2] == "1", sys.argv[3] == "1"
want = {
    "extraKnownMarketplaces": {
        "loki-mode": {"source": {"source": "github", "repo": "asklokesh/loki-mode"}}
    },
    "enabledPlugins": {"loki-mode@loki-mode": True},
}
if stop:
    want["env"] = {"LOKI_STOP_VERIFY": "1"}
if print_only:
    print(json.dumps(want, indent=2))
    sys.exit(0)
cur = {}
if os.path.exists(path):
    try:
        raw = open(path).read()
        cur = json.loads(raw) if raw.strip() else {}
    except Exception as e:
        print("REFUSED %s is not valid JSON (%s); fix or move it aside" % (path, e))
        sys.exit(3)
    if not isinstance(cur, dict):
        print("REFUSED %s top level is not an object" % path)
        sys.exit(3)
added = []
for top, kv in want.items():
    node = cur.get(top)
    if node is None:
        cur[top] = {}
        node = cur[top]
    elif not isinstance(node, dict):
        print("KEPT %s (existing value is not an object)" % top)
        continue
    for k, v in kv.items():
        if k in node:
            print("KEPT %s.%s" % (top, k))
        else:
            node[k] = v
            added.append("%s.%s" % (top, k))
            print("ADDED %s.%s" % (top, k))
if added:
    d = os.path.dirname(os.path.abspath(path))
    os.makedirs(d, exist_ok=True)
    fd, tmp = tempfile.mkstemp(dir=d, prefix=".settings.", suffix=".tmp")
    with os.fdopen(fd, "w") as f:
        json.dump(cur, f, indent=2)
        f.write("\n")
    os.replace(tmp, path)
else:
    print("UNCHANGED %s" % path)
PY
)"
    rc=$?
    printf '%s\n' "$out"
    [ "$rc" -eq 0 ] || return "$rc"
    [ "$print_only" -eq 1 ] || echo "Next: in Claude Code run /reload-plugins, then try /loki-mode:start, /loki-mode:loki-verify, /loki-mode:handoff."
    return 0
}

# Set up skill symlinks for all providers
cmd_setup_skill() {
    # v7.6.2 B-13 fix: --help must print help, not install skills.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- install Loki skill into provider CLIs${NC}"
            echo ""
            echo "Usage: loki setup-skill [options]"
            echo ""
            echo "Installs the Loki Mode skill into each detected provider CLI's"
            echo "skills directory: ~/.claude/skills/, ~/.codex/skills/, etc."
            echo ""
            echo "Options:"
            echo "  --claude-code  Merge the Loki plugin into Claude Code settings (see --claude-code --help)"
            echo "  --help, -h   Show this help and exit"
            echo ""
            echo "Side effects: writes symlinks/files under ~/.<provider>/skills/loki-mode/."
            return 0
            ;;
        --claude-code)
            shift
            _setup_skill_claude_code "$@"
            return $?
            ;;
    esac
    echo -e "${BOLD}Loki Mode Skill Setup${NC}"
    echo ""

    local skill_targets=(
        "$HOME/.claude/skills/loki-mode:Claude Code"
        "$HOME/.codex/skills/loki-mode:Codex CLI"
        "$HOME/.cline/skills/loki-mode:Cline CLI"
        "$HOME/.aider/skills/loki-mode:Aider CLI"
    )

    local created=0
    local skipped=0
    local failed=0

    for entry in "${skill_targets[@]}"; do
        local target_dir="${entry%%:*}"
        local target_name="${entry#*:}"
        local parent_dir
        parent_dir=$(dirname "$target_dir")
        local short_path="${target_dir/#$HOME/\~}"

        # Already exists and valid
        if [ -f "$target_dir/SKILL.md" ]; then
            echo -e "  ${GREEN}OK${NC}    $target_name  ${DIM}($short_path)${NC}"
            skipped=$((skipped + 1))
            continue
        fi

        # A dangling link is safe to repair. Any live path without SKILL.md is
        # operator-owned: `ln -sf SOURCE DIRECTORY` would otherwise create a
        # nested link and report success, while a regular file could be
        # replaced outright. Refuse both cases without changing the path.
        if [ -L "$target_dir" ] && [ ! -e "$target_dir" ]; then
            rm -f "$target_dir"
        fi

        if [ -e "$target_dir" ] || [ -L "$target_dir" ]; then
            echo -e "  ${RED}FAIL${NC}  $target_name  ${DIM}($short_path exists without SKILL.md; move it aside and re-run)${NC}"
            failed=$((failed + 1))
            continue
        fi

        # Create parent directory
        if [ ! -d "$parent_dir" ]; then
            mkdir -p "$parent_dir" 2>/dev/null || true
        fi

        # Create symlink to skill dir
        if ln -sf "$SKILL_DIR" "$target_dir" 2>/dev/null; then
            echo -e "  ${GREEN}NEW${NC}   $target_name  ${DIM}($short_path)${NC}"
            created=$((created + 1))
        else
            echo -e "  ${RED}FAIL${NC}  $target_name  ${DIM}($short_path)${NC}"
            failed=$((failed + 1))
        fi
    done

    echo ""
    if [ "$created" -gt 0 ]; then
        echo -e "${GREEN}Created $created new skill symlink(s).${NC}"
    fi
    if [ "$skipped" -gt 0 ]; then
        echo -e "${DIM}$skipped already installed.${NC}"
    fi
    if [ "$failed" -gt 0 ]; then
        echo -e "${RED}$failed failed (check permissions).${NC}"
        return 1
    fi
    return 0
}

# v7.4.14: cmd_self_update -- one upgrade command for everyone.
#
# Auto-detects which package manager installed `loki` (npm, bun, brew) by
# resolving the binary path, and runs the right upgrade command.
#
# `--to <mgr>` switches managers (installs via the new one, then uninstalls
# the old). Useful for npm users wanting to migrate to Bun (the v8.0.0
# direction).
#
# Usage:
#   loki self-update                Upgrade in-place via current manager
#   loki self-update --to bun       Switch from current to Bun
#   loki self-update --to npm       Switch from current to npm
#   loki self-update --to brew      Switch from current to Homebrew
#   loki self-update --check        Print detection result, installed vs latest version + exit 0
#
cmd_self_update() {
    local target_manager=""
    local check_only=false

    while [ $# -gt 0 ]; do
        case "$1" in
            --to)
                if [ -z "${2:-}" ]; then
                    echo -e "${RED}--to requires an argument: bun|npm|brew${NC}" >&2
                    return 1
                fi
                target_manager="$2"
                shift 2
                ;;
            --check)
                check_only=true
                shift
                ;;
            --help|-h)
                cat <<'EOF'
loki self-update -- detect manager and upgrade in place, or switch managers

Usage:
  loki self-update                    Upgrade via current manager
  loki self-update --to bun           Switch to Bun (recommended for v8)
  loki self-update --to npm           Switch to npm
  loki self-update --to brew          Switch to Homebrew
  loki self-update --check            Print detected manager, installed vs latest version, and exit
  loki self-update --help             Show this help

Auto-detection looks at the resolved path of the `loki` binary:
  ~/.bun/bin/loki                  -> bun
  /opt/homebrew/Cellar/loki-mode   -> brew (also /usr/local/Cellar)
  npm prefix + /bin/loki           -> npm
  anywhere else                    -> unknown

Switching managers installs via the new one first (so a failed install
does not leave you with no loki), then uninstalls the old one.
EOF
                return 0
                ;;
            *)
                echo -e "${RED}Unknown argument: $1${NC}" >&2
                echo "Run 'loki self-update --help' for usage." >&2
                return 1
                ;;
        esac
    done

    # Detect current install path. v7.4.15 fix: use BASH_SOURCE (the
    # running script's own path) instead of `command -v loki`. The latter
    # depended on the caller's PATH, which fails when the user invokes
    # loki via absolute path or a symlink directory that isn't on PATH.
    # BASH_SOURCE always resolves to the actual installed location.
    local current_mgr=""
    local script_real pkg_dir
    if command -v realpath >/dev/null 2>&1; then
        script_real=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")
    else
        script_real="${BASH_SOURCE[0]}"
    fi
    # autonomy/loki sits at <pkg_dir>/autonomy/loki -- walk two up to get pkg_dir.
    pkg_dir=$(dirname "$(dirname "$script_real")")

    # Also resolve the public binary path (what the user types). We try
    # `command -v loki` but tolerate failure -- $pkg_dir is sufficient.
    local resolved=""
    resolved=$(command -v loki 2>/dev/null || echo "${BASH_SOURCE[0]}")

    case "$pkg_dir" in
        */.bun/install/global/*|*/.bun/*)
            current_mgr="bun"
            ;;
        */Cellar/loki-mode/*|*/homebrew/Cellar/loki-mode/*|*/linuxbrew/Cellar/loki-mode/*)
            current_mgr="brew"
            ;;
        */node_modules/loki-mode*|*/lib/node_modules/loki-mode*)
            current_mgr="npm"
            ;;
        *)
            # Last resort: ask npm if it knows about a global loki-mode
            if command -v npm >/dev/null 2>&1; then
                local npm_root
                npm_root=$(npm root -g 2>/dev/null || true)
                if [ -n "$npm_root" ] && [[ "$pkg_dir" == "$npm_root"/* ]]; then
                    current_mgr="npm"
                fi
            fi
            ;;
    esac

    [ -z "$current_mgr" ] && current_mgr="unknown"

    if [ "$check_only" = "true" ]; then
        echo "loki binary:    $resolved"
        echo "package dir:    $pkg_dir"
        echo "current mgr:    $current_mgr"
        local installed_v latest_v="" update_flag="unknown"
        installed_v=$(get_version)
        latest_v=$(_loki_latest_published_version)
        if [ -z "$latest_v" ]; then
            echo "installed $installed_v, latest unknown"
        else
            if _loki_semver_newer "$latest_v" "$installed_v"; then update_flag="yes"; else update_flag="no"; fi
            echo "installed $installed_v, latest $latest_v, update available $update_flag"
        fi
        return 0
    fi

    # Default target: same as current (in-place upgrade)
    [ -z "$target_manager" ] && target_manager="$current_mgr"

    echo -e "${BOLD}loki self-update${NC}"
    echo "  current: $current_mgr ($pkg_dir)"
    echo "  target:  $target_manager"
    echo

    # Same-manager upgrade
    if [ "$target_manager" = "$current_mgr" ]; then
        case "$current_mgr" in
            npm)
                echo "Running: npm install -g loki-mode@latest"
                npm install -g loki-mode@latest
                ;;
            bun)
                echo "Running: bun update -g loki-mode (falling back to install if not present)"
                bun update -g loki-mode 2>/dev/null || bun install -g loki-mode@latest
                ;;
            brew)
                echo "Running: brew upgrade loki-mode"
                brew upgrade loki-mode
                ;;
            unknown)
                echo -e "${YELLOW}Cannot auto-detect package manager from: $pkg_dir${NC}" >&2
                if [ -d "$pkg_dir/.git" ]; then
                    echo "This looks like a git clone. To upgrade:" >&2
                    echo "  cd $pkg_dir && git pull && (cd loki-ts && bun install && bun run build)" >&2
                else
                    echo "Specify the target manager explicitly:" >&2
                    echo "  loki self-update --to bun" >&2
                    echo "  loki self-update --to npm" >&2
                    echo "  loki self-update --to brew" >&2
                fi
                return 1
                ;;
        esac
        local rc=$?
        if [ "$rc" -eq 0 ]; then
            echo
            echo -e "${GREEN}Upgrade complete.${NC} Verify with: loki version"
        fi
        return "$rc"
    fi

    # Cross-manager switch: install new, then uninstall old
    case "$target_manager" in
        bun)
            if ! command -v bun >/dev/null 2>&1; then
                echo -e "${RED}bun is not installed.${NC} Install it first:" >&2
                echo "  brew install oven-sh/bun/bun  # macOS via Homebrew" >&2
                echo "  curl -fsSL https://bun.sh/install | bash  # any platform" >&2
                return 1
            fi
            echo "Installing via Bun: bun install -g loki-mode@latest"
            bun install -g loki-mode@latest || return 1
            ;;
        npm)
            command -v npm >/dev/null 2>&1 || { echo -e "${RED}npm not found${NC}" >&2; return 1; }
            echo "Installing via npm: npm install -g loki-mode@latest"
            npm install -g loki-mode@latest || return 1
            ;;
        brew)
            command -v brew >/dev/null 2>&1 || { echo -e "${RED}brew not found${NC}" >&2; return 1; }
            echo "Tapping + installing via Homebrew"
            brew tap asklokesh/tap 2>/dev/null || true
            brew install loki-mode || brew upgrade loki-mode || return 1
            ;;
        *)
            echo -e "${RED}Unknown target manager: $target_manager${NC} (use bun, npm, or brew)" >&2
            return 1
            ;;
    esac

    # New install succeeded -- uninstall old. Best-effort; failures are
    # non-fatal because the user already has the new binary working.
    if [ "$current_mgr" != "unknown" ]; then
        echo
        echo "Removing old install via $current_mgr..."
        case "$current_mgr" in
            npm) npm uninstall -g loki-mode 2>/dev/null || true ;;
            bun) bun remove -g loki-mode 2>/dev/null || true ;;
            brew) brew uninstall loki-mode 2>/dev/null || true ;;
        esac
    fi

    echo
    echo -e "${GREEN}Switched from $current_mgr to $target_manager.${NC}"
    echo "New binary: $(command -v loki 2>/dev/null || echo 'restart your shell to pick up new PATH')"
    return 0
}

# FC-EGRESS-AUDIT-HAND-LISTED: the registry of every env var that names an outbound
# host. _loki_airgap_audit walks it, so a configured webhook, tracker, remote or model
# gateway is named in the audit. tests/test-airgap-commands.sh greps autonomy/,
# loki-ts/src, providers, dashboard and web-app for LOKI_*_(URL|HOST|ENDPOINT|WEBHOOK|ISSUER)
# and DATABASE_URL readers and fails on any name that
# is in neither list. Entry: NAME|what it reaches.
_LOKI_AIRGAP_EGRESS_ENV=(
    "LOKI_WEBHOOK_URL|custom webhook notifications"
    "LOKI_SLACK_WEBHOOK|Slack notifications"
    "LOKI_SLACK_WEBHOOK_URL|Slack run events (loki-ts)"
    "LOKI_DISCORD_WEBHOOK|Discord notifications"
    "LOKI_OTEL_ENDPOINT|OpenTelemetry trace export"
    "LOKI_QUEUE_URL|queue consumer broker (Redis)"
    "LOKI_METADATA_URL|metadata store"
    "LOKI_REMOTE_URL|remote build submission (loki start --remote)"
    "LOKI_MIROFISH_URL|MiroFish market-simulation service"
    "LOKI_HOSTED_ENDPOINT|loki proof share --hosted"
    "LOKI_CONTROL_URL|control plane shipping"
    "LOKI_CONTROL_PLANE_URL|control plane live line"
    "LOKI_JIRA_URL|Jira write-back"
    "JIRA_BASE_URL|Jira write-back (alias of LOKI_JIRA_URL)"
    "LOKI_LINEAR_URL|Linear write-back"
    "LOKI_MODEL_BASE_URL|model route base URL"
    "LOKI_AUDIT_SYSLOG_HOST|dashboard audit log forwarding to a remote syslog server"
    "LOKI_OIDC_ISSUER|dashboard OIDC sign-in (fetches the issuer discovery document and JWKS keys)"
    "DATABASE_URL|web-app Postgres database server (unset to use local file storage)"
)
# Names the guard skips because they never name an outbound host. NAME|reason.
_LOKI_AIRGAP_EGRESS_NOT=(
    "LOKI_POSTHOG_HOST|audited as the telemetry row"
    "LOKI_TELEMETRY_ENDPOINT|audited as the telemetry row"
    "LOKI_SLACK_WEBHOOK_ENV|the name of another env var, not a host"
    "LOKI_SENTRY_WEBHOOK_SECRET|a secret, not a host"
    "LOKI_SANDBOX_ALLOW_HOSTS|the sandbox's own allowlist, not a destination"
    "LOKI_PROOF_PUBLIC_URL|a link written into a proof page, never contacted"
    "LOKI_DEPLOYED_URL|a link written into a proof page, never contacted"
    "LOKI_PROOF_AUTHZ_DETAIL_URL_TEMPLATE|a path template for the app under test"
    "LOKI_TELEM_ENDPOINT|internal copy of LOKI_OTEL_ENDPOINT"
    "LOKI_BASE_URL|internal variable inside issue-providers.sh"
    "LOKI_CS_PR_URL|internal variable carrying a PR link this run created"
    "LOKI_DELEGATE_PR_URL|internal variable carrying a PR link this run created"
    "LOKI_PR_URL|internal variable carrying a PR link this run created"
    "LOKI_SENTRY_URL|internal variable carrying a Sentry permalink"
    "LOKI_HOST_GUARD_HOOK|internal path of the host-guard hook"
    "LOKI_HOST_GUARD_SETTINGS_JSON|internal settings blob of the host-guard hook"
    "LOKI_DASHBOARD_HOST|the dashboard's own bind address"
    "LOKI_HOST_GUARD|a boolean switch, not a host"
    "LOKI_DEPLOY_HEALTH_URL|set per run by loki deploy --health-url, never configuration"
    "LOKI_GIST_URL|internal variable carrying a gist link this run already created"
    "LOKI_LAST_GIST_URL|internal variable carrying a gist link this run already created"
    "LOKI_HOSTED_BODY|internal temp-file path"
    "LOKI_HOSTED_EP|internal copy of LOKI_HOSTED_ENDPOINT"
    "LOKI_DASHBOARD_ALLOWED_HOSTS|the dashboard's own inbound Host-header allowlist, not a destination"
    "LOKI_HOST_CHILD_PATH|a filesystem path for the host-guard sandbox, not a host"
    "LOKI_HOST_PROTECTED_ROOT|a filesystem path for the host-guard sandbox, not a host"
    "LOKI_HOST_RESERVED_PORTS|local ports the host-guard sandbox reserves, not a destination"
    "LOKI_HOST_RUNTIME_READ_PATHS|filesystem paths for the host-guard sandbox, not a host"
    "LOKI_HOST_RUNTIME_WRITE_PATHS|filesystem paths for the host-guard sandbox, not a host"
    "LOKI_HOST_SEATBELT|a boolean switch for the host-guard sandbox, not a host"
    "LOKI_HOST_SEATBELT_PROBE_SHA|a hash of the host-guard sandbox probe, not a host"
)

# _loki_airgap_configured_egress -- one TSV line per configured outbound host that is
# not part of the fixed rows: name, host (never the URL: webhook paths are secrets),
# loopback yes|no, how to disable it. Reads configuration only, no network.
# MCP servers with a url come from the three files an MCP launch can load
# (autonomy/lib/mcp-config.sh norm() keeps only command servers, so it cannot list these).
# Records are tab-separated lines, so no field may carry a control character: a crafted
# MCP server name or env value would otherwise split or forge rows. Every field is cleaned
# at the point it is emitted, and the JSON reader skips any line that is still malformed.
_loki_airgap_clean() { printf '%s' "$1" | LC_ALL=C tr '\001-\037\177' '?'; }
_loki_airgap_configured_egress() {
    local entry name what val host local_flag
    for entry in "${_LOKI_AIRGAP_EGRESS_ENV[@]}"; do
        name="${entry%%|*}"; what="${entry#*|}"
        val="${!name:-}"
        [ -n "$val" ] || continue
        host="$(loki_remote_host "$val")"
        [ -n "$host" ] || host="(unparseable value of $name)"
        local_flag="no"; loki_remote_is_local_host "$val" && local_flag="yes"
        printf '%s\t%s\t%s\tunset %s\n' "$what ($name)" "$(_loki_airgap_clean "$host")" "$local_flag" "$name"
    done
    local f
    for f in "${TARGET_DIR:-.}/.mcp.json" "$LOKI_DIR/mcp-config.json" "${HOME:-/nonexistent}/.claude/mcp.json"; do
        [ -f "$f" ] || continue
        python3 -I -c '
import json, os, re, sys
from urllib.parse import urlsplit
def clean(s):
    return re.sub(r"[\x00-\x1f\x7f\x85\u2028\u2029]", "?", str(s))
p = sys.argv[1]
try:
    if os.path.getsize(p) > 1048576:
        sys.exit(0)
    v = json.load(open(p, encoding="utf-8"))
except (OSError, ValueError):
    sys.exit(0)
for name, e in (v.get("mcpServers") or {}).items() if isinstance(v, dict) and isinstance(v.get("mcpServers"), dict) else []:
    u = e.get("url") if isinstance(e, dict) else None
    if not isinstance(u, str) or not u:
        continue
    try:
        h = urlsplit(u).hostname or ""
    except ValueError:
        h = ""
    print("MCP server %s (%s)\t%s\t%s\tremove the server from %s" % (clean(name), clean(p), clean(h or "(unparseable url)"), "yes" if h in ("localhost", "127.0.0.1", "::1") else "no", clean(p)))
' "$f" 2>/dev/null
    done
}

# Check system prerequisites
# _loki_airgap_audit <json?> -- enumerate every network egress this engine can
# perform, classify each as REQUIRED or optional, and say how to disable it.
#
# WHY THIS EXISTS
#   Air-gapped deployment is a real enterprise requirement and the incumbent
#   (Tabnine) states it can "be deployed in a completely air-gapped environment"
#   without publishing WHICH hosts it would otherwise contact. A claim you
#   cannot audit is a claim you have to take on faith.
#
#   Sourcegraph is the honest counter-example: their own whitepaper states that
#   self-hosted Cody still "sends requests from the Sourcegraph instance to
#   Anthropic's API". Self-hosted control plane, egressing inference.
#
#   So the credible position is not "trust us, we are air-gapped" -- it is here
#   is every host, here is which ones are required, here is how to turn each one
#   off, now verify it yourself with tcpdump. We would rather be auditable than
#   assertive.
#
# Exit 0 when no REQUIRED egress remains (air-gap achievable in this config),
# 1 otherwise. Deterministic: reads configuration, performs NO network calls.
_loki_airgap_audit() {
    local as_json="${1:-false}"

    # Provider inference endpoint: the one egress that is genuinely required,
    # unless the active provider serves weights locally.
    # Judge the provider `loki start` would use, with cmd_start's precedence:
    # the project's saved choice (.loki/state/provider, `loki provider set`)
    # wins over LOKI_PROVIDER. Reading only the env var reported a saved claude
    # project as air-gap ready. A --provider flag at start time is not visible
    # here; with nothing set, run.sh may auto-detect another installed CLI.
    local provider="" provider_src="$LOKI_DIR/state/provider"
    [ -f "$LOKI_DIR/state/provider" ] && provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
    if [ -z "$provider" ]; then
        provider_src="LOKI_PROVIDER"
        provider="${LOKI_PROVIDER:-}"
        [ -n "$provider" ] || { provider="claude"; provider_src="default"; }
    fi
    local model_host="" model_required="yes" model_note=""
    case "$provider" in
        claude)
            model_host="${ANTHROPIC_BASE_URL:-https://api.anthropic.com}"
            model_note="Claude Code inference. An in-network gateway (ANTHROPIC_BASE_URL) keeps it inside your network but is still counted as required egress here; only a local-weights provider clears it."
            ;;
        codex)
            model_host="${OPENAI_BASE_URL:-https://api.openai.com}"
            model_note="Codex inference. An in-network gateway (OPENAI_BASE_URL) is still counted as required egress here; only a local-weights provider clears it."
            ;;
        opencode|cline|aider)
            model_host="${OPENAI_BASE_URL:-${OPENROUTER_API_KEY:+https://openrouter.ai}}"
            # Local ONLY on positive evidence: a model id naming a local runtime.
            # An unset endpoint is not that evidence -- the opencode catalog
            # default is an openrouter/ model, so inference still leaves the host.
            # Judge only the ACTIVE provider's model variable (the one
            # providers/<provider>.sh reads): another provider's local model
            # says nothing about where this provider sends inference.
            local model_var model_id
            case "$provider" in
                opencode) model_var="LOKI_OPENCODE_MODEL" model_id="${LOKI_OPENCODE_MODEL:-}" ;;
                cline)    model_var="LOKI_CLINE_MODEL"    model_id="${LOKI_CLINE_MODEL:-}" ;;
                aider)    model_var="LOKI_AIDER_MODEL"    model_id="${LOKI_AIDER_MODEL:-}" ;;
            esac
            # An Ollama cloud model (tag ending ":cloud" or "-cloud") runs on
            # ollama.com through the local daemon (docs.ollama.com/cloud), so
            # naming ollama is not evidence of local inference regardless of
            # OLLAMA_HOST.
            if printf '%s' "$model_id" | grep -qiE '[:-]cloud$'; then
                model_host="https://ollama.com (Ollama cloud model $model_id)"
                model_note="Ollama cloud models run on ollama.com. Set $model_var to a local ollama/ model id (no :cloud or -cloud tag)."
            elif printf '%s' "$model_id" | grep -qiE 'ollama|localhost|127\.0\.0\.1|lmstudio'; then
                # A bare "ollama"/"lmstudio" in the model id is only local
                # evidence when the tool actually talks to this host. Ollama
                # honors OLLAMA_HOST to redirect its client at a remote
                # server, so a substring match on the model id alone is
                # forgeable: OLLAMA_HOST=https://remote.example.com with
                # model id "ollama/..." would egress to that remote host
                # while still reading as local. Parse OLLAMA_HOST's actual
                # host and only trust the substring when no override is set
                # or the override itself points at loopback.
                local ollama_host_local=1
                if [ -n "${OLLAMA_HOST:-}" ] && ! loki_remote_is_local_host "$OLLAMA_HOST"; then
                    ollama_host_local=0
                fi
                if [ "$ollama_host_local" = "1" ]; then
                    model_host="local weights"
                    model_required="no"
                    model_note="Local model runtime named by $model_var=$model_id. Judged from the model id (and OLLAMA_HOST when set): the endpoint set in the opencode, cline or aider config is not read, so confirm it is on this host or inside your network."
                else
                    model_host="$OLLAMA_HOST"
                    model_note="OLLAMA_HOST points at a remote host; inference leaves it. Unset OLLAMA_HOST or point it at loopback for zero-egress inference."
                fi
            else
                [ -n "$model_host" ] || model_host="remote model provider (no local model configured)"
                model_note="Remote model gateway. Set $model_var to an ollama/ model id for zero-egress inference."
            fi
            ;;
        *)
            model_host="unknown (provider '$provider')"
            model_note="Unrecognized provider; audit its config manually."
            ;;
    esac

    # LOKI_ALLOW_CLAUDE_SIDECALLS=1 lets engine side-calls prompt the claude
    # CLI under another provider: inference to Anthropic whatever the model.
    # Same policy helper the side-call sites use (providers/loader.sh).
    local sidecall_required="no" sidecall_host="${ANTHROPIC_BASE_URL:-https://api.anthropic.com}"
    type loki_claude_sidecall_allowed >/dev/null 2>&1 \
        || . "$_LOKI_SCRIPT_DIR/../providers/loader.sh" 2>/dev/null
    if [ "$provider" != "claude" ] && command -v claude >/dev/null 2>&1 \
       && loki_claude_sidecall_allowed "$provider" 2>/dev/null; then
        sidecall_required="yes"
    fi
    local blocked="$model_required"
    [ "$sidecall_required" = "yes" ] && blocked="yes"

    # Optional egress, each individually disableable.
    # Telemetry: the same gate main() used to send this command's own event.
    # It is ON by default for an individual interactive install, so a
    # hardcoded "off" misreported the one egress this audit itself triggers.
    local tel_state="off"
    if declare -f _loki_telemetry_enabled >/dev/null 2>&1 && _loki_telemetry_enabled; then
        tel_state="on"
    fi
    local tel_host
    tel_host="$(loki_remote_host "${LOKI_POSTHOG_HOST:-https://us.i.posthog.com}")"
    local upd_state="on"
    [ "${LOKI_NO_UPDATE_CHECK:-0}" = "1" ] && upd_state="off"

    local cfg_egress
    cfg_egress="$(_loki_airgap_configured_egress)"

    if [ "$as_json" = "true" ]; then
        _AG_PROV="$provider" _AG_PSRC="$provider_src" _AG_HOST="$model_host" _AG_REQ="$model_required" \
        _AG_NOTE="$model_note" _AG_TEL="$tel_state" _AG_TELHOST="$tel_host" _AG_UPD="$upd_state" \
        _AG_SC="$sidecall_required" _AG_SCHOST="$sidecall_host" _AG_CFG="$cfg_egress" \
        python3 -c '
import json, os
req = os.environ["_AG_REQ"] == "yes"
egress = [{
    "name": "model_inference",
    "host": os.environ["_AG_HOST"],
    "required": req,
    "disable": "use a local-weights provider (opencode, cline or aider with a local ollama/ or lmstudio/ model id)",
    "note": os.environ["_AG_NOTE"],
}, {
    "name": "telemetry",
    "host": os.environ["_AG_TELHOST"],
    "required": False,
    "enabled": os.environ["_AG_TEL"] == "on",
    "disable": "loki telemetry off, DO_NOT_TRACK=1, or LOKI_AIRGAP=true (on by default for interactive installs; auto-off in CI)",
}, {
    "name": "update_check",
    "host": "registry.npmjs.org",
    "required": False,
    "enabled": os.environ["_AG_UPD"] == "on",
    "disable": "export LOKI_NO_UPDATE_CHECK=1",
}]
for line in os.environ["_AG_CFG"].split("\n"):  # never splitlines(): it also breaks on U+0085, U+2028 and U+2029
    parts = line.split("\t")
    if len(parts) != 4:
        continue
    n, h, loc, dis = parts
    egress.append({"name": n, "host": h, "required": False, "enabled": True, "loopback": loc == "yes", "disable": dis})
if os.environ["_AG_SC"] == "yes":
    egress.append({
        "name": "claude_sidecalls",
        "host": os.environ["_AG_SCHOST"],
        "required": True,
        "disable": "unset LOKI_ALLOW_CLAUDE_SIDECALLS",
        "note": "LOKI_ALLOW_CLAUDE_SIDECALLS=1 lets engine steps prompt the claude CLI under this provider",
    })
blockers = [e for e in egress if e.get("required")]
print(json.dumps({
    "provider": os.environ["_AG_PROV"],
    "provider_source": os.environ["_AG_PSRC"],
    "airgap_ready": not blockers,
    "required_egress": [e["name"] for e in blockers],
    "egress": egress,
    "not_audited": [{
        "name": "package_installs",
        "note": "app runner (npm, pip, docker) for the built app, dashboard venv (PyPI), quality-gate npx for tools not installed locally; pre-install or mirror them, a failed npx fetch fails that gate",
        "disable": "LOKI_APP_RUNNER=false, LOKI_DASHBOARD=false",
    }, {
        "name": "delegate_pr",
        "note": "git push and gh pr create to your remote after a successful run, when gh is installed",
        "disable": "LOKI_DELEGATE_PR=0",
    }],
}, indent=2))
'
        [ "$blocked" = "yes" ] && return 1
        return 0
    fi

    echo -e "${BOLD}Network egress audit${NC}"
    echo ""
    echo "  Active provider: $provider (from $provider_src)"
    echo ""
    echo -e "${BOLD}Egress points${NC}"
    if [ "$model_required" = "yes" ]; then
        echo -e "  ${YELLOW}REQUIRED${NC}  model inference -> $model_host"
    else
        echo -e "  ${GREEN}none${NC}      model inference -> $model_host"
    fi
    echo "            $model_note"
    if [ "$sidecall_required" = "yes" ]; then
        echo -e "  ${YELLOW}REQUIRED${NC}  claude side-calls -> $sidecall_host"
        echo "            LOKI_ALLOW_CLAUDE_SIDECALLS=1 lets engine steps prompt the claude CLI under $provider."
    fi
    echo -e "  optional  telemetry           [$tel_state]  -> $tel_host"
    echo "            disable: loki telemetry off, DO_NOT_TRACK=1, or LOKI_AIRGAP=true (on by default for interactive installs)"
    echo -e "  optional  update check        [$upd_state]  disable: export LOKI_NO_UPDATE_CHECK=1"
    if [ -n "$cfg_egress" ]; then
        local c_name c_host c_loc c_dis
        while IFS=$'\t' read -r c_name c_host c_loc c_dis; do
            [ -n "$c_name" ] || continue
            if [ "$c_loc" = "yes" ]; then
                echo "  optional  $c_name -> $c_host (loopback)"
            else
                echo "  optional  $c_name -> $c_host"
            fi
            echo "            disable: $c_dis"
        done <<< "$cfg_egress"
    fi
    echo ""
    echo -e "${BOLD}Not audited here${NC} (depends on the project; see docs/air-gapped.md)"
    echo "  package installs  app runner (npm, pip, docker), dashboard venv (PyPI), quality-gate"
    echo "                    npx for tools not installed locally. Pre-install or mirror them;"
    echo "                    a failed npx fetch fails that gate."
    echo "                    disable: LOKI_APP_RUNNER=false, LOKI_DASHBOARD=false"
    echo "  delegate PR       git push and gh pr create to your remote after a successful run,"
    echo "                    when gh is installed. disable: LOKI_DELEGATE_PR=0"
    echo ""
    if [ "$sidecall_required" = "yes" ] && [ "$model_required" != "yes" ]; then
        echo -e "${YELLOW}Not air-gap ready in this configuration.${NC}"
        echo "  One required egress remains: claude side-calls."
        echo "  To close it: unset LOKI_ALLOW_CLAUDE_SIDECALLS"
        return 1
    fi
    if [ "$model_required" = "yes" ]; then
        echo -e "${YELLOW}Not air-gap ready in this configuration.${NC}"
        echo "  One required egress remains: model inference."
        [ "$sidecall_required" = "yes" ] && echo "  (Also unset LOKI_ALLOW_CLAUDE_SIDECALLS: claude side-calls are required egress too.)"
        echo "  To close it, serve the model locally:"
        echo "    loki provider set opencode"
        echo "    export LOKI_OPENCODE_MODEL=ollama/qwen2.5-coder"
        echo ""
        echo "  Then re-run: loki doctor --airgap"
        return 1
    fi
    echo -e "${GREEN}Air-gap ready: no required egress in this configuration.${NC}"
    echo "  Verify independently -- run with the interface down, or watch with tcpdump."
    return 0
}

# _loki_bun_delegate <cmd> [args...]
# Hand a command to its single Bun implementation (FC-DUP, docs/v10/FAILURE-CLASSES.md).
# Resolves bun the way bin/loki's _loki_bun does (PATH first, then an npm-installed
# @oven/bun-* package, including bun.exe on Windows), resolves the CLI (dist, then
# src), preflights `bun <cli> version` under a 5s timeout (when `timeout` exists),
# then execs. On success it never returns. On failure it returns 1 and sets
# _LOKI_DELEGATE_WHY (the single reason) and _LOKI_DELEGATE_FIX (one fix line) so
# the caller can print a minimal diagnostic.
_loki_bun_delegate() {
    local cmd="$1"
    shift
    local root="${_LOKI_SCRIPT_DIR}/.."
    local c bun_bin="" cli="" t="" rc=0 found=0
    _LOKI_DELEGATE_WHY=""
    _LOKI_DELEGATE_FIX=""

    for c in "$(command -v bun 2>/dev/null || true)" \
             "$root"/node_modules/@oven/bun-*/bin/bun \
             "$root"/node_modules/@oven/bun-*/bin/bun.exe \
             "$root/node_modules/bun/bin/bun.exe"; do
        [ -n "$c" ] && [ -x "$c" ] || continue
        found=1
        if "$c" --version >/dev/null 2>&1; then
            bun_bin="$c"
            break
        fi
    done
    if [ -z "$bun_bin" ]; then
        if [ "$found" = "1" ]; then
            _LOKI_DELEGATE_WHY="bun failed to start (found but 'bun --version' exited non-zero)"
            _LOKI_DELEGATE_FIX="Reinstall bun: curl -fsSL https://bun.sh/install | bash"
        else
            _LOKI_DELEGATE_WHY="bun missing (not on PATH and no @oven/bun-* package under ${root})"
            _LOKI_DELEGATE_FIX="Install bun: curl -fsSL https://bun.sh/install | bash (or npm install -g bun)"
        fi
        return 1
    fi

    if [ -n "${LOKI_TS_ENTRY:-}" ]; then
        if [ ! -f "$LOKI_TS_ENTRY" ]; then
            _LOKI_DELEGATE_WHY="LOKI_TS_ENTRY invalid (${LOKI_TS_ENTRY} does not exist)"
            _LOKI_DELEGATE_FIX="Unset LOKI_TS_ENTRY or point it at a built loki.js"
            return 1
        fi
        cli="$LOKI_TS_ENTRY"
    elif [ -f "$root/loki-ts/dist/loki.js" ]; then
        cli="$root/loki-ts/dist/loki.js"
        # Dev checkout: a gitignored dist older than src is stale (same rule as bin/loki).
        if [ -f "$root/loki-ts/src/cli.ts" ] && [ -n "$(find "$root/loki-ts/src" -type f -newer "$cli" -print -quit 2>/dev/null)" ]; then
            cli="$root/loki-ts/src/cli.ts"
        fi
    elif [ -f "$root/loki-ts/src/cli.ts" ]; then
        cli="$root/loki-ts/src/cli.ts"
    else
        _LOKI_DELEGATE_WHY="dist missing (no loki-ts/dist/loki.js or loki-ts/src/cli.ts under ${root})"
        _LOKI_DELEGATE_FIX="Reinstall loki-mode (npm install -g loki-mode) or run: cd loki-ts && bun run build"
        return 1
    fi

    t="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)"
    if [ -n "$t" ]; then
        "$t" -k 1 5 "$bun_bin" --no-env-file --config=/dev/null "$cli" version >/dev/null 2>&1
    else
        "$bun_bin" --no-env-file --config=/dev/null "$cli" version >/dev/null 2>&1
    fi
    rc=$?
    if [ "$rc" -ne 0 ]; then
        _LOKI_DELEGATE_WHY="bun failed exit ${rc} running ${cli} version"
        _LOKI_DELEGATE_FIX="Reinstall loki-mode (npm install -g loki-mode) or unset LOKI_TS_ENTRY"
        return 1
    fi

    exec "$bun_bin" --no-env-file --config=/dev/null "$cli" "$cmd" "$@"
}

# One-line fix for a requirement id (a provider name is an id), read from the shared
# table autonomy/lib/setup-requirements.json so runtime errors match loki doctor.
_loki_req_fix_line() {
    local _id="$1" _fix=""
    _fix="$(jq -r --arg id "$_id" '.requirements[] | select(.id == $id) | .fix' "${SKILL_DIR}/autonomy/lib/setup-requirements.json" 2>/dev/null | head -n 1)" || _fix=""
    if [ -n "$_fix" ]; then
        echo "  $_fix"
    else
        echo "  Check the provider documentation for installation."
    fi
}

# loki doctor. The one implementation is loki-ts/src/commands/doctor.ts; this is a
# delegator. Only --airgap stays in bash (_loki_airgap_audit). If the Bun route is
# unavailable we print a minimal diagnostic (never a second copy of the checks).
cmd_doctor() {
    local json_output=false
    local airgap_audit=false
    local a
    for a in "$@"; do
        case "$a" in
            --json) json_output=true ;;
            --airgap) airgap_audit=true ;;
        esac
    done

    if [ "$airgap_audit" = true ]; then
        _loki_airgap_audit "$json_output"
        return $?
    fi

    # LOKI_LEGACY_BASH stays set: doctor.ts reads it to report the bash runtime
    # and print the warning; it never re-routes on it.
    _loki_bun_delegate doctor "$@" || true

    # Reached only when the delegate could not exec.
    if [ "$json_output" = true ]; then
        cmd_doctor_json
    else
        echo "loki doctor: the Bun doctor is unavailable, so only this minimal report can run."
        echo "Fix: ${_LOKI_DELEGATE_FIX}"
        echo "${_LOKI_DELEGATE_WHY}"
        ( source "$SKILL_DIR/providers/model_routes.sh" 2>/dev/null && loki_model_route_lane_report ) || true
    fi
    return 1
}

# Minimal --json fallback when the Bun doctor is unavailable. Same top-level keys as
# DoctorJson (loki-ts/src/commands/doctor.ts) with summary.ok=false, failed=1, plus
# route:"unavailable" and the reason.
cmd_doctor_json() {
    local ver why fix
    ver="$(get_version 2>/dev/null || echo unknown)"
    why="${_LOKI_DELEGATE_WHY:-bun route unavailable}"
    fix="${_LOKI_DELEGATE_FIX:-}"
    why="${why//\\/\\\\}"; why="${why//\"/\\\"}"
    fix="${fix//\\/\\\\}"; fix="${fix//\"/\\\"}"
    ver="${ver//\\/\\\\}"; ver="${ver//\"/\\\"}"
    local reqs=null
    if declare -F loki_requirements_json >/dev/null 2>&1; then
        reqs="$(loki_requirements_json 2>/dev/null | jq -c . 2>/dev/null)" || reqs=null
        [ -n "$reqs" ] || reqs=null
    fi
    printf '{"loki_mode_version":"%s","checks":[],"provider_availability":null,"disk":null,"ai_provider":null,"skills":[],"sentrux":null,"memory":null,"model_catalog":null,"agent_sdk":{"status":"unknown","pinned":null,"installed":null,"detail":"not checked: the Bun doctor is unavailable"},"requirements":%s,"summary":{"passed":0,"failed":1,"warnings":0,"ok":false},"route":"unavailable","reason":"%s","fix":"%s"}\n' "$ver" "$reqs" "$why" "$fix"
}

# Architectural-drift gate (v7.5.14, opt-in).
#
# Wraps the external sentrux Rust binary -- https://github.com/sentrux/sentrux --
# to give users a deterministic per-iteration architecture-drift signal that
# complements the existing 11 quality gates and 3-reviewer council. This is a
# manual subcommand only in v7.5.14; iteration-loop integration is deferred
# pending a real-PRD smoke test (tracked in CHANGELOG).
#
# Subcommands:
#   loki sentrux baseline [<path>]   -- save .sentrux/baseline.json
#   loki sentrux gate     [<path>]   -- compare current vs baseline
#   loki sentrux status   [<path>]   -- print current baseline + verdict
#
# Default path is the current working directory.
cmd_sentrux() {
    # shellcheck source=autonomy/lib/sentrux-gate.sh
    if ! source "$_LOKI_SCRIPT_DIR/lib/sentrux-gate.sh" 2>/dev/null \
       && ! source "$(dirname "$0")/lib/sentrux-gate.sh" 2>/dev/null; then
        echo -e "${RED}sentrux helper missing -- expected at autonomy/lib/sentrux-gate.sh${NC}" >&2
        return 1
    fi

    local sub="${1:-help}"
    if [ "$#" -gt 0 ]; then shift; fi

    # Parse --force flag (used by init-rules); collect remaining args into target.
    local force=0
    local positional=()
    while [ "$#" -gt 0 ]; do
        case "$1" in
            --force|-f) force=1; shift ;;
            --) shift; while [ "$#" -gt 0 ]; do positional+=("$1"); shift; done ;;
            *) positional+=("$1"); shift ;;
        esac
    done
    local target="${positional[0]:-.}"

    case "$sub" in
        baseline)
            if ! sentrux_available; then
                echo -e "${YELLOW}sentrux not installed.${NC} Install with one of:" >&2
                echo "  brew install sentrux/tap/sentrux" >&2
                echo "  curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh" >&2
                echo "  See also: https://github.com/sentrux/sentrux" >&2
                return 2
            fi
            if sentrux_baseline_save "$target"; then
                local q
                q=$(sentrux_baseline_quality "$target" || echo "?")
                echo -e "${GREEN}Baseline saved.${NC} Quality: $q (path: $target)"
                return 0
            fi
            echo -e "${RED}Failed to save baseline.${NC}" >&2
            return 1
            ;;
        gate)
            if ! sentrux_available; then
                echo -e "${YELLOW}sentrux not installed.${NC} Install with one of:" >&2
                echo "  brew install sentrux/tap/sentrux" >&2
                echo "  curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh" >&2
                echo "  See also: https://github.com/sentrux/sentrux" >&2
                return 2
            fi
            local diff verdict before after
            diff=$(sentrux_gate_diff "$target")
            verdict=$(printf '%s' "$diff" | awk -F'|' '{print $3}')
            before=$(printf '%s' "$diff" | awk -F'|' '{print $1}')
            after=$(printf '%s' "$diff" | awk -F'|' '{print $2}')
            case "$verdict" in
                OK)
                    echo -e "${GREEN}OK${NC}  Quality: $before -> $after (no degradation)"
                    return 0
                    ;;
                DEGRADED)
                    echo -e "${RED}DEGRADED${NC}  Quality: $before -> $after"
                    echo "  Architecture regressed since the saved baseline."
                    echo "  Inspect with: sentrux scan $target"
                    return 1
                    ;;
                *)
                    echo -e "${YELLOW}UNKNOWN${NC}  Could not parse sentrux output."
                    echo "  Try: sentrux gate --save $target  (to refresh baseline)"
                    return 2
                    ;;
            esac
            ;;
        status)
            if ! sentrux_available; then
                echo "sentrux: not installed (optional)"
                echo "install with one of:"
                echo "  brew install sentrux/tap/sentrux"
                echo "  curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh"
                echo "  See also: https://github.com/sentrux/sentrux"
                return 0
            fi
            local v
            v=$(sentrux_version || echo "unknown")
            echo "sentrux: v$v"
            local q
            if q=$(sentrux_baseline_quality "$target"); then
                echo "baseline ($target): quality=$q"
            else
                echo "baseline ($target): not saved (run: loki sentrux baseline $target)"
            fi
            return 0
            ;;
        init-rules)
            # Scaffold a conservative default .sentrux/rules.toml in <target>/.sentrux/.
            # Does not require sentrux binary -- the file is plain text.
            local rules_dir="$target/.sentrux"
            local rules_file="$rules_dir/rules.toml"
            local abs_path
            if [ -e "$rules_file" ] && [ "$force" -ne 1 ]; then
                echo -e "${YELLOW}Refusing to overwrite existing $rules_file${NC}" >&2
                echo "  Re-run with --force to replace it." >&2
                return 1
            fi
            if ! mkdir -p "$rules_dir" 2>/dev/null; then
                echo -e "${RED}Failed to create $rules_dir${NC}" >&2
                return 2
            fi
            if ! cat > "$rules_file" <<'SENTRUX_RULES_EOF'
# Sentrux architectural rules scaffolded by `loki sentrux init-rules` (Loki Mode v7.5.15).
# Conservative defaults -- tighten per-project as needed.
# See https://github.com/sentrux/sentrux for full spec.

[constraints]
# Block any iteration that introduces an import cycle.
max_cycles = 0
# Block files that grow into "god files" (very high churn + many dependents).
no_god_files = true
# Cap cyclomatic complexity per function (sentrux default is liberal).
max_cc = 30

# Layer enforcement is project-specific. Uncomment + edit when you know the
# layout you want enforced. Example for a typical app:
#
# [[layers]]
# name = "core"
# paths = ["src/core/*"]
# order = 0
#
# [[layers]]
# name = "app"
# paths = ["src/app/*"]
# order = 2
#
# [[boundaries]]
# from = "src/app/*"
# to = "src/core/internal/*"
# reason = "app must not depend on core internals"
SENTRUX_RULES_EOF
            then
                echo -e "${RED}Failed to write $rules_file${NC}" >&2
                return 2
            fi
            # Resolve absolute path for friendly output (portable across macOS/Linux).
            if command -v python3 >/dev/null 2>&1; then
                abs_path=$(python3 -c "import os,sys; print(os.path.abspath(sys.argv[1]))" "$rules_file" 2>/dev/null || echo "$rules_file")
            else
                abs_path=$(cd "$(dirname "$rules_file")" 2>/dev/null && pwd)/$(basename "$rules_file")
            fi
            echo -e "${GREEN}Wrote $abs_path${NC}"
            echo "Edit it to add layer/boundary rules, then run: loki sentrux baseline $target"
            return 0
            ;;
        help|--help|-h|"")
            echo -e "${BOLD}loki sentrux${NC} - Architectural drift gate (opt-in, requires sentrux binary)"
            echo ""
            echo "Usage:"
            echo "  loki sentrux baseline   [<path>]         Save current architecture as baseline"
            echo "  loki sentrux gate       [<path>]         Compare current vs baseline (exit 1 on DEGRADED)"
            echo "  loki sentrux status     [<path>]         Show binary version + saved baseline quality"
            echo "  loki sentrux init-rules [<path>] [--force]"
            echo "                                           Scaffold a default .sentrux/rules.toml"
            echo ""
            echo "Default path is the current directory."
            echo ""
            echo "About:"
            echo "  Wraps the sentrux CLI (https://github.com/sentrux/sentrux), a Rust tool that"
            echo "  scores codebase structure into a single 0-10000 quality signal. Useful as a"
            echo "  supplement to Loki's existing 11 quality gates when you want an objective"
            echo "  per-iteration architectural-drift number."
            echo ""
            echo "  Iteration-loop auto-gating is planned for a future release; for now this"
            echo "  subcommand is the manual integration surface."
            echo ""
            echo "Install sentrux:"
            echo "  brew install sentrux/tap/sentrux"
            echo "  curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh"
            return 0
            ;;
        *)
            echo -e "${RED}Unknown subcommand: $sub${NC}" >&2
            echo "Run 'loki sentrux help' for usage." >&2
            return 1
            ;;
    esac
}

# Show version
cmd_version() {
    # v7.6.2 B-13 fix: --help prints help; bare invocation prints version.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- print version${NC}"
            echo ""
            echo "Usage: loki version [--json]"
            echo ""
            echo "Prints 'Loki Mode vX.Y.Z' from the VERSION file shipped with"
            echo "the package. Equivalent to 'loki --version' and 'loki -v'."
            echo ""
            echo "  --json    Print {\"version\",\"runtime\",\"install_path\"} as one JSON line"
            return 0
            ;;
        --json)
            local _vp="${SKILL_DIR:-}"
            _vp="${_vp//\\/\\\\}"
            _vp="${_vp//\"/\\\"}"
            printf '{"version":"%s","runtime":"bash","install_path":"%s"}\n' "$(get_version)" "$_vp"
            return 0
            ;;
    esac
    echo "Loki Mode v$(get_version)"
    # Best-effort stale-install nudge (stderr only; never blocks; off on
    # non-TTY/CI). Mirrors the Bun route's update check for parity.
    maybe_print_update_hint
}

# Secrets / credential management
cmd_secrets() {
    case "${1:-status}" in
        status)
            echo "API Key Status:"
            echo ""
            for key_var in ANTHROPIC_API_KEY OPENAI_API_KEY GOOGLE_API_KEY; do
                local value="${!key_var:-}"
                if [[ -n "$value" ]]; then
                    local masked="${value:0:8}...${value: -4}"
                    echo "  $key_var: SET ($masked, ${#value} chars)"
                else
                    # Check secret file mounts
                    local lower_name
                    lower_name=$(echo "$key_var" | tr '[:upper:]' '[:lower:]')
                    local found=false
                    for mount_path in /run/secrets /var/run/secrets; do
                        if [[ -f "$mount_path/$lower_name" ]]; then
                            echo "  $key_var: AVAILABLE (secret file: $mount_path/$lower_name)"
                            found=true
                            break
                        fi
                    done
                    if [[ "$found" != "true" ]]; then
                        echo "  $key_var: NOT SET"
                    fi
                fi
            done
            ;;
        validate)
            echo "Validating API keys..."
            python3 -c "
import sys
sys.path.insert(0, '$(dirname "$(dirname "$(resolve_script_path "$0")")")')
from dashboard.secrets import load_secrets
result = load_secrets()
for name, info in result.items():
    status = 'OK' if info['valid_format'] else 'INVALID'
    source = info['source']
    masked = info['masked']
    warning = info.get('warning', '')
    if info['set']:
        print(f'  {name}: {status} ({masked}, source: {source})')
        if warning:
            print(f'    WARNING: {warning}')
    else:
        print(f'  {name}: NOT SET')
" 2>/dev/null || echo "  (Python validation unavailable, showing basic status)"
            ;;
        help)
            echo "Usage: loki secrets [status|validate|help]"
            echo ""
            echo "Manage API keys and credentials."
            echo ""
            echo "Commands:"
            echo "  status    Show which API keys are configured (default)"
            echo "  validate  Validate API key formats"
            echo "  help      Show this help"
            echo ""
            echo "Secret Loading Priority:"
            echo "  1. Environment variable (ANTHROPIC_API_KEY, etc.)"
            echo "  2. Docker/K8s secret file (/run/secrets/anthropic_api_key)"
            echo "  3. Not set"
            ;;
        *)
            echo "Unknown secrets command: $1"
            cmd_secrets help
            return 1
            ;;
    esac
}

# Show recent logs
cmd_logs() {
    local lines="50"
    local follow=false

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --tail|-n) lines="$2"; shift 2 ;;
            --tail=*) lines="${1#--tail=}"; shift ;;
            -n=*) lines="${1#-n=}"; shift ;;
            --follow|-f) follow=true; shift ;;
            --all|-a) lines="+1"; shift ;;
            --help|-h)
                echo "Usage: loki logs [options]"
                echo ""
                echo "Options:"
                echo "  --tail, -n N   Show last N lines (default: 50)"
                echo "  --all, -a      Show all lines"
                echo "  --follow, -f   Follow log output (like tail -f)"
                echo "  --help, -h     Show this help"
                return 0
                ;;
            *)
                # Legacy: treat plain number as line count
                if [[ "$1" =~ ^[0-9]+$ ]]; then
                    lines="$1"
                fi
                shift
                ;;
        esac
    done

    # Resolve the log the runner ACTUALLY writes.
    #
    # This read a logs/session.log path that nothing in the tree writes (other
    # readers of it exist in web-app/ and examples/api/, all equally dead). So
    # `loki logs`, the
    # most natural "what is it doing?" command, reported "No log file found"
    # while the real logs sat in that same directory. run.sh:22409 writes
    # .loki/logs/autonomy-YYYYMMDD.log per day, plus agent.log for the dashboard.
    #
    # Newest dated log first; agent.log as the fallback. Both are listed in the
    # not-found message so the user can see where we looked rather than being
    # told a single wrong path.
    local log_dir="$LOKI_DIR/logs"
    local log_file=""
    if [ -d "$log_dir" ]; then
        # `|| log_file=""` is load-bearing: under `set -euo pipefail`, ls exits 2
        # when the glob matches nothing and pipefail propagates it, aborting the
        # function before the agent.log fallback and before the not-found message.
        # Without it, a host with agent.log but no dated log exits 1 in SILENCE --
        # a worse failure than the wrong-path bug this replaced.
        log_file="$(ls -1t "$log_dir"/autonomy-*.log 2>/dev/null | head -1)" || log_file=""
        [ -n "$log_file" ] || { [ -f "$log_dir/agent.log" ] && log_file="$log_dir/agent.log"; }
    fi

    if [ -z "$log_file" ] || [ ! -f "$log_file" ]; then
        echo -e "${YELLOW}No log file found in $log_dir${NC}"
        echo -e "${DIM}Looked for: autonomy-YYYYMMDD.log (newest), then agent.log${NC}"
        echo -e "${DIM}Logs are written once a run starts: loki start <spec>${NC}"
        exit 0
    fi

    echo -e "${DIM}Log: $log_file${NC}"

    if [ "$follow" = true ]; then
        echo -e "${BOLD}Following logs (Ctrl+C to stop)${NC}"
        echo ""
        tail -f "$log_file"
    else
        echo -e "${BOLD}Recent Logs (last $lines lines)${NC}"
        echo -e "${DIM}Use --all to show all lines, --follow to stream${NC}"
        echo ""
        tail -n "$lines" "$log_file"
    fi
}

# API server management (delegates to unified FastAPI dashboard server).
# Shares the SAME machine-global PID/control dir as `loki dashboard`
# ($DASHBOARD_PID_DIR = ~/.loki/dashboard) so the two commands never fight
# over the port and so stop/status/open are consistent across both. Parses
# --host/--port, guards a busy port, computes the TLS scheme, and persists
# host/port/scheme side-files like cmd_dashboard_start does.
cmd_api() {
    local subcommand="${1:-help}"
    shift || true
    local port="${LOKI_DASHBOARD_PORT:-$DASHBOARD_DEFAULT_PORT}"
    local host="${LOKI_DASHBOARD_HOST:-$DASHBOARD_DEFAULT_HOST}"
    local pid_file="$DASHBOARD_PID_FILE"

    # Parse --host/--port (the old code ignored them, so `loki serve --port X`
    # silently bound 57374 and `--host 0.0.0.0` was dropped).
    # A trailing --help/-h on any subcommand (e.g. `loki api start --help`)
    # short-circuits to the help text instead of being swallowed and starting
    # the server (#574).
    while [ $# -gt 0 ]; do
        case "$1" in
            --help|-h) subcommand="help"; break ;;
            --port) port="${2:-$port}"; shift 2 ;;
            --port=*) port="${1#*=}"; shift ;;
            --host) host="${2:-$host}"; shift 2 ;;
            --host=*) host="${1#*=}"; shift ;;
            *) shift ;;
        esac
    done

    local scheme="http"
    if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
        scheme="https"
    fi
    # Loopback-reachable host for printed URLs when bound to 0.0.0.0.
    local url_host="$host"; [ "$url_host" = "0.0.0.0" ] && url_host="127.0.0.1"

    case "$subcommand" in
        start)
            # Check if Python is available
            if ! command -v python3 &> /dev/null; then
                echo -e "${RED}Error: Python 3 not found. Install with: brew install python3${NC}"
                exit 1
            fi

            # Check if already running
            if [ -f "$pid_file" ]; then
                local existing_pid=$(cat "$pid_file" 2>/dev/null)
                if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
                    echo -e "${YELLOW}Dashboard server already running (PID: $existing_pid)${NC}"
                    echo "URL: ${scheme}://${url_host}:$port"
                    exit 0
                fi
                rm -f "$pid_file"
            fi

            # Refuse a busy port instead of writing a bogus PID.
            if command -v lsof &> /dev/null && lsof -nP -iTCP:"$port" -sTCP:LISTEN &> /dev/null; then
                echo -e "${RED}Error: Port $port is already in use${NC}"
                echo "Stop the other server or set LOKI_DASHBOARD_PORT / --port to a free port."
                exit 1
            fi

            # Ensure dashboard Python dependencies
            if ! ensure_dashboard_venv; then
                echo -e "${RED}Cannot start dashboard without Python dependencies${NC}"
                exit 1
            fi
            local api_python="$DASHBOARD_PYTHON"

            # Start server. Persist host/port/scheme so status/open are accurate.
            mkdir -p "$LOKI_DIR/logs" "$DASHBOARD_PID_DIR"
            local uvicorn_args=("--host" "$host" "--port" "$port")
            if [ "$scheme" = "https" ]; then
                uvicorn_args+=("--ssl-certfile" "${LOKI_TLS_CERT}" "--ssl-keyfile" "${LOKI_TLS_KEY}")
            fi
            LOKI_DIR="$LOKI_DIR" LOKI_SKILL_DIR="$SKILL_DIR" PYTHONPATH="$SKILL_DIR" \
                LOKI_DASHBOARD_HOST="$host" LOKI_DASHBOARD_PORT="$port" \
                nohup "$api_python" -m uvicorn dashboard.server:app "${uvicorn_args[@]}" > "$LOKI_DIR/logs/api.log" 2>&1 &
            local new_pid=$!
            echo "$new_pid" > "$pid_file"
            echo "$port" > "${DASHBOARD_PID_DIR}/port"
            echo "$host" > "${DASHBOARD_PID_DIR}/host"
            echo "$scheme" > "${DASHBOARD_PID_DIR}/scheme"

            echo -e "${GREEN}Dashboard server started${NC}"
            echo "  PID:  $new_pid"
            echo "  URL:  ${scheme}://${url_host}:$port"
            echo "  Logs: $LOKI_DIR/logs/api.log"
            ;;

        stop)
            if [ -f "$pid_file" ]; then
                local pid=$(cat "$pid_file")
                if kill -0 "$pid" 2>/dev/null; then
                    kill "$pid"
                    rm -f "$pid_file"
                    echo -e "${GREEN}Dashboard server stopped${NC}"
                else
                    rm -f "$pid_file"
                    echo -e "${YELLOW}Dashboard server was not running${NC}"
                fi
            else
                echo -e "${YELLOW}No dashboard server PID file found${NC}"
            fi
            ;;

        status)
            if [ -f "$pid_file" ]; then
                local pid=$(cat "$pid_file")
                if kill -0 "$pid" 2>/dev/null; then
                    # Prefer persisted side-files for the real bind.
                    local s_scheme="$scheme" s_host="$url_host" s_port="$port"
                    [ -f "${DASHBOARD_PID_DIR}/scheme" ] && s_scheme="$(cat "${DASHBOARD_PID_DIR}/scheme" 2>/dev/null)"
                    [ -f "${DASHBOARD_PID_DIR}/host" ] && s_host="$(cat "${DASHBOARD_PID_DIR}/host" 2>/dev/null)"
                    [ -f "${DASHBOARD_PID_DIR}/port" ] && s_port="$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)"
                    [ "$s_host" = "0.0.0.0" ] && s_host="127.0.0.1"
                    echo -e "${GREEN}Dashboard server running${NC}"
                    echo "  PID:  $pid"
                    echo "  URL:  ${s_scheme}://${s_host}:${s_port}"

                    # Try to fetch status (best-effort; may 401 under auth)
                    if command -v curl &> /dev/null; then
                        echo ""
                        echo -e "${CYAN}Status:${NC}"
                        local _k=""; [ "$s_scheme" = "https" ] && _k="-k"
                        curl -s $_k "${s_scheme}://${s_host}:${s_port}/api/status" 2>/dev/null | jq . 2>/dev/null || true
                    fi
                else
                    echo -e "${YELLOW}Dashboard server not running (stale PID file)${NC}"
                    rm -f "$pid_file"
                fi
            else
                echo -e "${YELLOW}Dashboard server not running${NC}"
            fi
            ;;

        help|--help|-h)
            echo -e "${BOLD}Loki Mode Dashboard/API Server${NC}"
            echo ""
            echo "Usage: loki api <command>"
            echo ""
            echo "Commands:"
            echo "  start    Start the unified FastAPI dashboard server"
            echo "  stop     Stop the dashboard server"
            echo "  status   Check if dashboard server is running"
            echo ""
            echo "Environment:"
            echo "  LOKI_DASHBOARD_PORT  Port for the API server"
            echo ""
            echo "Endpoints:"
            echo "  GET  /api/health  - Health check"
            echo "  GET  /api/status  - Session status"
            echo "  GET  /api/events  - SSE stream"
            echo "  GET  /api/logs    - Recent logs"
            echo "  POST /api/start   - Start session"
            echo "  POST /api/stop    - Stop session"
            echo "  POST /api/pause   - Pause session"
            echo "  POST /api/resume  - Resume session"
            ;;
        *)
            echo "loki api: unknown subcommand '${subcommand}' (see 'loki api help')" >&2
            return 2
            ;;
    esac
}

# Multi-channel notifications (Slack, Discord, Webhook)
cmd_notify() {
    local subcommand="${1:-help}"
    shift || true

    case "$subcommand" in
        test)
            cmd_notify_test "$@"
            ;;
        slack)
            cmd_notify_send "slack" "$@"
            ;;
        discord)
            cmd_notify_send "discord" "$@"
            ;;
        webhook)
            cmd_notify_send "webhook" "$@"
            ;;
        desktop)
            cmd_notify_send "desktop" "$@"
            ;;
        status)
            cmd_notify_status
            ;;
        --help|-h|help)
            cmd_notify_help
            ;;
        *)
            # If first arg looks like a message, treat as test
            if [[ "$subcommand" != -* ]]; then
                cmd_notify_test "$subcommand" "$@"
            else
                echo -e "${RED}Unknown notify command: $subcommand${NC}"
                echo "Run 'loki notify help' for usage."
                exit 1
            fi
            ;;
    esac
}

cmd_notify_help() {
    echo -e "${BOLD}Loki Mode Notifications${NC}"
    echo ""
    echo "Usage: loki notify <command> [message]"
    echo ""
    echo "Commands:"
    echo "  test [message]     Send test notification to all configured channels"
    echo "  slack <message>    Send notification to Slack only"
    echo "  discord <message>  Send notification to Discord only"
    echo "  webhook <message>  Send notification to webhook only"
    echo "  desktop <message>  Send a desktop notification (macOS or Linux notify-send)"
    echo "  test --channel desktop [message]  Test only the desktop channel"
    echo "  status             Show which notification channels are configured"
    echo "  help               Show this help"
    echo ""
    echo "Environment Variables:"
    echo "  LOKI_SLACK_WEBHOOK    Slack incoming webhook URL"
    echo "  LOKI_DISCORD_WEBHOOK  Discord webhook URL"
    echo "  LOKI_WEBHOOK_URL      Custom webhook URL (POST JSON, signed)"
    echo "  LOKI_WEBHOOK_SECRET   HMAC-SHA256 key for X-Loki-Signature (required to send)"
    echo "  LOKI_NOTIFY_CHANNELS  Channels to use (comma-separated: slack,discord,webhook,desktop)"
    echo "                        Default: all configured channels"
    echo ""
    echo "Examples:"
    echo "  loki notify test                    # Test all configured channels"
    echo "  loki notify test 'Hello World'     # Test with custom message"
    echo "  loki notify slack 'Build complete' # Send to Slack only"
    echo "  loki notify status                 # Check configuration"
    echo ""
    echo "Configuration via .loki/config.yaml:"
    echo "  notifications:"
    echo "    slack_webhook: https://hooks.slack.com/services/..."
    echo "    discord_webhook: https://discord.com/api/webhooks/..."
    echo "    webhook_url: https://your-server.com/webhook"
    echo "    channels: slack,discord  # or 'all'"
}

cmd_notify_status() {
    echo -e "${BOLD}Notification Channel Status${NC}"
    echo ""

    local any_configured=false

    # Check Slack
    if [ -n "${LOKI_SLACK_WEBHOOK:-}" ]; then
        echo -e "  ${GREEN}[OK]${NC} Slack     - Configured"
        any_configured=true
    else
        echo -e "  ${DIM}[--]${NC} Slack     - Not configured (set LOKI_SLACK_WEBHOOK)"
    fi

    # Check Discord
    if [ -n "${LOKI_DISCORD_WEBHOOK:-}" ]; then
        echo -e "  ${GREEN}[OK]${NC} Discord   - Configured"
        any_configured=true
    else
        echo -e "  ${DIM}[--]${NC} Discord   - Not configured (set LOKI_DISCORD_WEBHOOK)"
    fi

    # Check custom webhook
    if [ -n "${LOKI_WEBHOOK_URL:-}" ] && [ -z "${LOKI_WEBHOOK_SECRET:-}" ]; then
        echo -e "  ${YELLOW}[!!]${NC} Webhook   - URL set but LOKI_WEBHOOK_SECRET is missing, so nothing is sent"
    elif [ -n "${LOKI_WEBHOOK_URL:-}" ]; then
        echo -e "  ${GREEN}[OK]${NC} Webhook   - Configured (signed)"
        any_configured=true
    else
        echo -e "  ${DIM}[--]${NC} Webhook   - Not configured (set LOKI_WEBHOOK_URL)"
    fi

    echo ""

    # Check channel filter
    if [ -n "${LOKI_NOTIFY_CHANNELS:-}" ]; then
        echo -e "${CYAN}Active channels:${NC} ${LOKI_NOTIFY_CHANNELS}"
    else
        echo -e "${CYAN}Active channels:${NC} all configured"
    fi

    echo ""

    if [ "$any_configured" = "false" ]; then
        echo -e "${YELLOW}No notification channels configured.${NC}"
        echo "Set environment variables or add to .loki/config.yaml"
    else
        echo -e "Test with: ${CYAN}loki notify test${NC}"
    fi
}

# Desktop channel: reuse run.sh send_notification (the same function the run
# loop fires on blocked, done and failed) by extracting it, so the macOS and
# Linux logic lives in one place. Returns 1 when no desktop notifier exists.
send_desktop_notification() {
    local message="$1" title="${2:-Notification}"
    command -v osascript >/dev/null 2>&1 || command -v notify-send >/dev/null 2>&1 || return 1
    [ -f "$RUN_SH" ] || return 1
    local fn
    fn="$(sed -n '/^send_notification() {/,/^}/p' "$RUN_SH")"
    [ -n "$fn" ] || return 1
    (
        # shellcheck disable=SC2034
        NOTIFICATIONS_ENABLED=true
        # shellcheck disable=SC2034
        NOTIFICATION_SOUND=false
        eval "$fn"
        send_notification "$title" "$message" "normal"
    )
}

cmd_notify_test() {
    local channel_arg=""
    if [ "${1:-}" = "--channel" ]; then
        channel_arg="${2:-}"
        shift 2 2>/dev/null || shift $#
    fi
    local message="${1:-Loki Mode test notification}"
    local channels_notified=0
    [ -n "$channel_arg" ] && LOKI_NOTIFY_CHANNELS="$channel_arg"

    echo -e "${BOLD}Testing Notification Channels${NC}"
    echo ""

    # Determine which channels to test
    local channels="${LOKI_NOTIFY_CHANNELS:-all}"

    # Test Slack
    if [[ "$channels" == "all" || "$channels" == *"slack"* ]]; then
        if [ -n "${LOKI_SLACK_WEBHOOK:-}" ]; then
            echo -n "  Slack...    "
            if send_slack_notification "$message" "Test"; then
                echo -e "${GREEN}OK${NC}"
                channels_notified=$((channels_notified + 1))
            else
                echo -e "${RED}FAILED${NC}"
            fi
        fi
    fi

    # Test Discord
    if [[ "$channels" == "all" || "$channels" == *"discord"* ]]; then
        if [ -n "${LOKI_DISCORD_WEBHOOK:-}" ]; then
            echo -n "  Discord...  "
            if send_discord_notification "$message" "Test"; then
                echo -e "${GREEN}OK${NC}"
                channels_notified=$((channels_notified + 1))
            else
                echo -e "${RED}FAILED${NC}"
            fi
        fi
    fi

    # Test custom webhook
    if [[ "$channels" == "all" || "$channels" == *"webhook"* ]]; then
        if [ -n "${LOKI_WEBHOOK_URL:-}" ]; then
            echo -n "  Webhook...  "
            if send_webhook_notification "$message" "Test"; then
                echo -e "${GREEN}OK${NC}"
                channels_notified=$((channels_notified + 1))
            else
                echo -e "${RED}FAILED${NC}"
            fi
        fi
    fi

    # Desktop is opt-in (explicit channel) so default runs never pop a banner
    if [[ "${channels}" == *"desktop"* ]]; then
        echo -n "  Desktop...  "
        if send_desktop_notification "$message" "Test"; then
            echo -e "${GREEN}OK${NC}"
            channels_notified=$((channels_notified + 1))
        else
            echo -e "${RED}FAILED (no osascript or notify-send)${NC}"
        fi
    fi

    echo ""

    if [ $channels_notified -eq 0 ]; then
        echo -e "${YELLOW}No channels configured or enabled.${NC}"
        echo "Run 'loki notify status' to check configuration."
        exit 1
    else
        echo -e "${GREEN}Sent to $channels_notified channel(s)${NC}"
    fi
}

cmd_notify_send() {
    local channel="$1"
    shift
    local message="$*"

    if [ -z "$message" ]; then
        echo -e "${RED}Error: Message required${NC}"
        echo "Usage: loki notify $channel <message>"
        exit 2
    fi

    case "$channel" in
        slack)
            if [ -z "${LOKI_SLACK_WEBHOOK:-}" ]; then
                echo -e "${RED}Error: LOKI_SLACK_WEBHOOK not set${NC}"
                exit 1
            fi
            if send_slack_notification "$message" "Manual"; then
                echo -e "${GREEN}Sent to Slack${NC}"
            else
                echo -e "${RED}Failed to send to Slack${NC}"
                exit 1
            fi
            ;;
        discord)
            if [ -z "${LOKI_DISCORD_WEBHOOK:-}" ]; then
                echo -e "${RED}Error: LOKI_DISCORD_WEBHOOK not set${NC}"
                exit 1
            fi
            if send_discord_notification "$message" "Manual"; then
                echo -e "${GREEN}Sent to Discord${NC}"
            else
                echo -e "${RED}Failed to send to Discord${NC}"
                exit 1
            fi
            ;;
        webhook)
            if [ -z "${LOKI_WEBHOOK_URL:-}" ]; then
                echo -e "${RED}Error: LOKI_WEBHOOK_URL not set${NC}"
                exit 1
            fi
            if [ -z "${LOKI_WEBHOOK_SECRET:-}" ]; then
                echo -e "${RED}Error: LOKI_WEBHOOK_SECRET not set (webhooks are signed; nothing sent)${NC}"
                exit 1
            fi
            if send_webhook_notification "$message" "Manual"; then
                echo -e "${GREEN}Sent to webhook${NC}"
            else
                echo -e "${RED}Failed to send to webhook${NC}"
                exit 1
            fi
            ;;
        desktop)
            if send_desktop_notification "$message" "Manual"; then
                echo -e "${GREEN}Sent desktop notification${NC}"
            else
                echo -e "${RED}No desktop notifier (osascript or notify-send) available${NC}"
                exit 1
            fi
            ;;
    esac
}

# Send notification to Slack via incoming webhook
send_slack_notification() {
    local message="$1"
    local event_type="${2:-Notification}"
    local webhook_url="${LOKI_SLACK_WEBHOOK:-}"

    if [ -z "$webhook_url" ]; then
        return 1
    fi

    local project_name
    project_name=$(basename "$(pwd)")
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local message_escaped event_escaped project_escaped
    message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g')
    event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g')
    project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g')

    # Build Slack payload with blocks for better formatting
    local payload
    payload=$(cat <<EOF
{
    "blocks": [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "Loki Mode: $event_escaped"
            }
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "$message_escaped"
            }
        },
        {
            "type": "context",
            "elements": [
                {
                    "type": "mrkdwn",
                    "text": "*Project:* $project_escaped | *Time:* $timestamp"
                }
            ]
        }
    ]
}
EOF
)

    # Send to Slack
    curl -s -X POST -H 'Content-type: application/json' \
        --data "$payload" \
        "$webhook_url" > /dev/null 2>&1
}

# Send notification to Discord via webhook
send_discord_notification() {
    local message="$1"
    local event_type="${2:-Notification}"
    local webhook_url="${LOKI_DISCORD_WEBHOOK:-}"

    if [ -z "$webhook_url" ]; then
        return 1
    fi

    local project_name
    project_name=$(basename "$(pwd)")
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local message_escaped event_escaped project_escaped
    message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g')
    event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g')
    project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g')

    # Build Discord embed payload
    local payload
    payload=$(cat <<EOF
{
    "embeds": [
        {
            "title": "Loki Mode: $event_escaped",
            "description": "$message_escaped",
            "color": 5814783,
            "footer": {
                "text": "Project: $project_escaped | $timestamp"
            }
        }
    ]
}
EOF
)

    # Send to Discord
    curl -s -X POST -H 'Content-type: application/json' \
        --data "$payload" \
        "$webhook_url" > /dev/null 2>&1
}

# HMAC-SHA256 of stdin keyed by LOKI_WEBHOOK_SECRET, lowercase hex. The secret is
# read from the environment inside python3 so it never appears in argv or logs.
_webhook_hmac_hex() {
    python3 -c 'import hashlib,hmac,os,sys;print(hmac.new(os.environ["LOKI_WEBHOOK_SECRET"].encode(),sys.stdin.buffer.read(),hashlib.sha256).hexdigest())'
}

# Send a signed lifecycle event to the custom webhook (POST JSON, schema_version 1).
# Header X-Loki-Signature: sha256=<hex HMAC of the exact body>. No LOKI_WEBHOOK_SECRET
# means nothing is sent. The payload carries the event name and project name only:
# no message text, no prompt text, no absolute paths.
send_webhook_notification() {
    local message="$1"
    local event_type="${2:-Notification}"
    local webhook_url="${LOKI_WEBHOOK_URL:-}"

    if [ -z "$webhook_url" ]; then
        return 1
    fi
    if [ -z "${LOKI_WEBHOOK_SECRET:-}" ] || ! command -v python3 >/dev/null 2>&1; then
        echo "loki: webhook not sent: set LOKI_WEBHOOK_SECRET to sign outbound webhooks" >&2
        return 1
    fi

    local timestamp event_escaped project_escaped payload sig
    timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g')
    project_escaped=$(printf '%s' "$(basename "$(pwd)")" | sed 's/\\/\\\\/g; s/"/\\"/g')
    payload=$(printf '{"schema_version":1,"source":"loki-mode","event":"%s","project":"%s","timestamp":"%s"}' \
        "$event_escaped" "$project_escaped" "$timestamp")
    sig=$(printf '%s' "$payload" | _webhook_hmac_hex) || return 1

    printf '%s' "$payload" | curl -s --connect-timeout 5 --max-time 10 -X POST -H 'Content-type: application/json' \
        -H "X-Loki-Signature: sha256=${sig}" \
        --data-binary @- \
        "$webhook_url" > /dev/null 2>&1
}

# Broadcast notification to all configured channels
# Used by run.sh for automated notifications
broadcast_notification() {
    local message="$1"
    local event_type="${2:-Notification}"
    local channels="${LOKI_NOTIFY_CHANNELS:-all}"

    # Send to each enabled channel (silently, for background use)
    if [[ "$channels" == "all" || "$channels" == *"slack"* ]]; then
        send_slack_notification "$message" "$event_type" 2>/dev/null || true
    fi

    if [[ "$channels" == "all" || "$channels" == *"discord"* ]]; then
        send_discord_notification "$message" "$event_type" 2>/dev/null || true
    fi

    if [[ "$channels" == "all" || "$channels" == *"webhook"* ]]; then
        send_webhook_notification "$message" "$event_type" 2>/dev/null || true
    fi
}

# Sandbox management (delegates to sandbox.sh)
# sandbox.sh handles detection: Docker Desktop Sandbox > Docker Container > Worktree
cmd_sandbox() {
    local subcommand="${1:-help}"
    shift || true

    if [ ! -f "$SANDBOX_SH" ]; then
        echo -e "${RED}Error: sandbox.sh not found at $SANDBOX_SH${NC}"
        echo "Expected at: $SKILL_DIR/autonomy/sandbox.sh"
        exit 1
    fi

    # Delegate to sandbox.sh (it handles Docker/worktree detection internally)
    exec "$SANDBOX_SH" "$subcommand" "$@"
}

# v7.29.0 (feature #4): print the SIMPLE-tier cost/time/iteration estimate for
# the demo PRD, parsed from the single estimator source (show_prd_plan JSON).
# Because `loki demo` always runs `loki start --simple` (which exports
# LOKI_COMPLEXITY=simple), the estimate is computed with LOKI_COMPLEXITY=simple
# so the quoted figures are the ones the demo will actually incur -- honest by
# construction (the keystone fix makes show_prd_plan honor LOKI_COMPLEXITY).
# Returns 0 when a real estimate was printed, 1 when the estimator failed (the
# caller then falls back to a no-number confirm). Never fabricates a number.
emit_demo_estimate() {
    local prd_path="$1"
    # v7.30.0 (item 5): the demo estimate always prints before spending, incl.
    # non-TTY (--dry-run / --yes piped), so gate color on a TTY here too (the
    # global vars are only blanked on NO_COLOR). Mirrors provider-offer.sh and
    # quickstart.sh. Local overrides only.
    local BOLD="$BOLD" YELLOW="$YELLOW" DIM="$DIM" NC="$NC"
    if [ ! -t 1 ]; then
        BOLD=''; YELLOW=''; DIM=''; NC=''
    fi
    local plan_json=""
    plan_json=$(LOKI_COMPLEXITY=simple show_prd_plan "$prd_path" "true" "false" 2>/dev/null) || plan_json=""

    if [ -z "$plan_json" ]; then
        echo -e "${YELLOW}Could not compute a cost estimate (the estimator did not return a result).${NC}"
        return 1
    fi

    local parsed
    parsed=$(printf '%s' "$plan_json" | python3 -c "
import json, sys
try:
    d = json.load(sys.stdin)
except Exception:
    sys.exit(1)
cost = d.get('cost', {}).get('total_usd')
time_est = d.get('time', {}).get('estimated')
iters = d.get('iterations', {}).get('estimated')
rng = d.get('iterations', {}).get('range', [])
tier = d.get('complexity', {}).get('tier', 'simple')
if cost is None or time_est is None or iters is None:
    sys.exit(1)
rng_str = ''
if isinstance(rng, list) and len(rng) == 2:
    rng_str = ' (range {}-{})'.format(rng[0], rng[1])
print(tier.upper())
print('{:.2f}'.format(float(cost)))
print(time_est)
print('{}{}'.format(iters, rng_str))
" 2>/dev/null) || parsed=""

    if [ -z "$parsed" ]; then
        echo -e "${YELLOW}Could not compute a cost estimate (the estimator did not return a result).${NC}"
        return 1
    fi

    local tier_u cost_u time_u iter_u
    tier_u=$(printf '%s' "$parsed" | sed -n '1p')
    cost_u=$(printf '%s' "$parsed" | sed -n '2p')
    time_u=$(printf '%s' "$parsed" | sed -n '3p')
    iter_u=$(printf '%s' "$parsed" | sed -n '4p')

    echo -e "${BOLD}Estimate (${tier_u} tier, the path this demo actually runs):${NC}"
    printf '  Cost:        ~$%s\n' "$cost_u"
    echo "  Time:        ~$time_u"
    echo "  Iterations:  $iter_u"
    echo ""
    echo -e "${DIM}This is an estimate. Actual usage depends on PRD complexity,"
    echo -e "code review cycles, and test failures.${NC}"
    echo ""
    return 0
}

# loki wow: a tiny real Loki 10 task in a temp repo, or the offline tour when no provider is ready.
wow_offline_finish() {
    echo "No live provider for this run. Showing the offline sample (no key, no spend)."
    echo ""
    cmd_demo --offline
    echo ""
    echo "Ready for your repo? loki start"
}

# Heuristic, not an auth check: an installed claude CLI with a key, an OAuth token,
# or a config dir counts as ready. A logged-out install passes this and then fails
# the live run; cmd_wow reports that failure and its exit code, it never hides it.
wow_provider_ready() {
    local p="${1:-}"
    [ -n "$p" ] || p="${LOKI_PROVIDER:-claude}"
    case "$p" in
        claude)
            command -v claude >/dev/null 2>&1 || return 1
            if [ -n "${ANTHROPIC_API_KEY:-}${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then return 0; fi
            [ -e "${HOME}/.claude.json" ] || [ -d "${HOME}/.claude" ] || return 1
            return 0
            ;;
        opencode|codex|cline|aider)
            command -v "$p" >/dev/null 2>&1
            ;;
        *)
            return 1
            ;;
    esac
}

# Prints the newest receipt under $1/.loki/runs. Returns 1 when none was written.
wow_show_receipt() {
    local demo_dir="$1" receipt vrc=0
    receipt="$(python3 - "$demo_dir/.loki/runs" <<'PY'
import os, sys
root, best = sys.argv[1], None
for d, _, files in os.walk(root):
    if "receipt.json" in files:
        p = os.path.join(d, "receipt.json")
        if best is None or os.path.getmtime(p) > os.path.getmtime(best):
            best = p
print(best or "")
PY
)" || receipt=""
    if [ -z "$receipt" ]; then
        return 1
    fi
    echo ""
    echo "Receipt: $receipt"
    python3 - "$receipt" <<'PY'
import json, sys
try:
    with open(sys.argv[1], encoding="utf-8") as fh:
        r = json.load(fh)
    if not isinstance(r, dict):
        raise ValueError("not a JSON object")
except (OSError, ValueError) as exc:
    print(f"Outcome: unreadable receipt ({exc.__class__.__name__}); loki verify below is the check")
    sys.exit(0)
verdict = str(r.get("verdict") or "unknown")
cost = r.get("cost") if isinstance(r.get("cost"), dict) else {}
usd = cost.get("usd")
tokens = 0
for k in ("input_tokens", "output_tokens", "cache_read_tokens", "cache_creation_tokens"):
    try:
        tokens += int(cost.get(k) or 0)
    except (TypeError, ValueError):
        pass
np = r.get("not_proven") if isinstance(r.get("not_proven"), list) else []
print(f"Outcome: {verdict}")
print(f"Cost: ${usd:.4f}" if isinstance(usd, (int, float)) else "Cost: not recorded on the receipt")
print(f"Tokens: {tokens}")
print("PROVEN: sealed verdict VERIFIED" if verdict == "VERIFIED" else f"PROVEN: no verified claim (verdict {verdict})")
print("NOT PROVEN:")
for item in np or ["none"]:
    print(f"  - {item}")
PY
    echo ""
    echo "Verify offline:"
    (cd "$demo_dir" && "$SKILL_DIR/bin/loki" verify) || vrc=$?
    if [ "$vrc" = "0" ]; then
        echo "loki verify exited 0"
    else
        echo "loki verify exited $vrc (the receipt did not check out; that is the evidence)"
    fi
    return 0
}

# Removes only a directory the caller created: a direct child of the temp root named
# <prefix>.* (default loki-wow), not a symlink. With a marker file name as $3, the directory
# must also hold that file (not a symlink) whose first line equals the directory's own path.
wow_cleanup_dir() {
    local d="${1:-}" prefix="${2:-loki-wow}" marker="${3:-}" root marker_value=""
    [ -n "$d" ] && [ -d "$d" ] && [ ! -L "$d" ] || return 0
    root="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || return 0
    case "$d" in "$root/$prefix".*) ;; *) return 0 ;; esac
    [ "$(dirname -- "$d")" = "$root" ] || return 0
    if [ -n "$marker" ]; then
        [ -f "$d/$marker" ] && [ ! -L "$d/$marker" ] || return 0
        IFS= read -r marker_value <"$d/$marker" || true
        [ "$marker_value" = "$d" ] || return 0
    fi
    rm -rf -- "$d"
}

cmd_wow() {
    local offline=false dry_run=false keep=false provider="" max_cost="2"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo "loki wow - a tiny real task in a temporary git repo"
                echo ""
                echo "Creates a temp repo, runs one small fix through the Loki 10 engine,"
                echo "seals a receipt, and prints PROVEN vs NOT PROVEN, cost and tokens."
                echo "With no provider key it runs the offline sample instead (loki tour)."
                echo "A failed live run shows its own receipt and exit code, never the sample."
                echo ""
                echo "Usage: loki wow [--offline] [--dry-run] [--keep] [--provider P] [--max-cost N]"
                echo ""
                echo "  --offline     Always show the offline sample"
                echo "  --dry-run     Print the plan. No model call, no spend"
                echo "  --keep        Keep the temp repo (default: removed after the receipt is printed)"
                echo "  --provider P  claude (default) or opencode, codex, cline, aider"
                echo "  --max-cost N  Hard per-run cap in USD, a positive number (default 2). budgets.per_run still applies"
                echo ""
                echo "loki tour stays the offline sample. loki tour --live is this command."
                echo "Ready for your repo? loki start"
                return 0
                ;;
            --offline) offline=true; shift ;;
            --dry-run) dry_run=true; shift ;;
            --keep) keep=true; shift ;;
            --provider)
                provider="${2:-}"
                if [ -z "$provider" ]; then echo "loki wow: --provider requires a value" >&2; return 1; fi
                shift 2
                ;;
            --provider=*) provider="${1#*=}"; shift ;;
            --max-cost)
                max_cost="${2:-}"
                if [ -z "$max_cost" ]; then echo "loki wow: --max-cost requires a value" >&2; return 1; fi
                shift 2
                ;;
            --max-cost=*) max_cost="${1#*=}"; shift ;;
            *)
                echo "loki wow: unknown option: $1" >&2
                echo "Run 'loki wow --help'." >&2
                return 1
                ;;
        esac
    done
    if ! [[ "$max_cost" =~ ^[0-9]+(\.[0-9]+)?$ ]] || ! awk -v c="$max_cost" 'BEGIN { exit !(c > 0) }'; then
        echo "loki wow: --max-cost must be a positive number of USD, got: $max_cost" >&2
        return 2
    fi

    if [ "$offline" = true ]; then
        wow_offline_finish
        return $?
    fi

    local use_provider="${provider:-${LOKI_PROVIDER:-claude}}"
    case "$use_provider" in
        claude|opencode|codex|cline|aider) ;;
        *) echo "loki wow: unknown provider '$use_provider' (use claude, opencode, codex, cline or aider)" >&2; return 2 ;;
    esac
    if ! wow_provider_ready "$use_provider"; then
        if [ "$dry_run" = true ]; then
            echo "loki wow: no provider ready for '$use_provider'. A real run would show the offline sample."
            echo "Ready for your repo? loki start"
            return 0
        fi
        wow_offline_finish
        return $?
    fi

    local task='Make greet() in greet.js return the string hello so `node --test greet.test.js` passes. Do not change the test.'
    echo "loki wow: temp repo, provider $use_provider, max-cost \$$max_cost, no PR (local receipt)."
    echo "Task: $task"
    if [ "$dry_run" = true ]; then
        echo "[dry-run] Would create a git repo with a failing node:test and run:"
        echo "  loki start \"<task>\" --no-pr --provider $use_provider --max-cost $max_cost"
        echo "Then print the receipt, PROVEN vs NOT PROVEN, and: Ready for your repo? loki start"
        return 0
    fi

    local demo_dir temp_root
    temp_root="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || { echo "loki wow: cannot resolve the temp root" >&2; return 1; }
    demo_dir="$(mktemp -d "${temp_root}/loki-wow.XXXXXXXX")" || return 1
    if [ "$keep" != true ]; then
        # shellcheck disable=SC2064  # expand demo_dir now; it is local to this call
        trap "wow_cleanup_dir '$demo_dir'; trap - INT TERM; exit 130" INT TERM
    fi
    printf '%s\n' '{ "type": "module", "name": "loki-wow" }' > "$demo_dir/package.json"
    printf '%s\n' 'export function greet() { return "hi"; }' > "$demo_dir/greet.js"
    printf '%s\n' \
        'import assert from "node:assert/strict";' \
        'import test from "node:test";' \
        'import { greet } from "./greet.js";' \
        'test("greet says hello", () => { assert.equal(greet(), "hello"); });' > "$demo_dir/greet.test.js"
    # Inherited GIT_DIR and friends would point -C at the caller's repo; signing or hooks could fail the commit.
    if ! (
        unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_PREFIX
        g() { git -c commit.gpgsign=false -c core.hooksPath=/dev/null -C "$demo_dir" "$@"; }
        g init -q && g config user.email "loki-wow@example.com" && g config user.name "Loki Wow" \
            && g add package.json greet.js greet.test.js && g commit -q -m "failing greet"
    ); then
        echo "loki wow: could not set up the temp git repo" >&2
        if [ "$keep" != true ]; then trap - INT TERM; wow_cleanup_dir "$demo_dir"; fi
        return 1
    fi
    echo "Directory: $demo_dir"
    local rc=0
    (
        cd "$demo_dir" && LOKI_ASSUME_YES=1 "$SKILL_DIR/bin/loki" start "$task" --no-pr --provider "$use_provider" --max-cost "$max_cost"
    ) || rc=$?
    if ! wow_show_receipt "$demo_dir"; then
        echo ""
        if [ "$rc" != "0" ]; then
            echo "The live mini-task exited $rc before a receipt was sealed (often a provider that is installed but not logged in)."
            echo "Nothing below is the result of this run. It is the offline sample, for reference."
            echo ""
            cmd_demo --offline
        else
            echo "No receipt file was written. The run summary above is the record."
        fi
    elif [ "$rc" != "0" ]; then
        echo ""
        echo "The live mini-task exited $rc. The receipt above is this run's own result."
    fi
    if [ "$keep" = true ]; then
        echo ""
        echo "Kept: $demo_dir (remove it with: rm -rf \"$demo_dir\")"
    else
        trap - INT TERM
        wow_cleanup_dir "$demo_dir"
    fi
    echo ""
    echo "Ready for your repo? loki start"
    return "$rc"
}

cmd_factory() {
    if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
        echo "loki factory \"<task|prd|issue>\" - same as loki start"
        echo ""
        echo "loki start prints the autonomy policy and runs. factory does not change that."
        echo "It does not write a policy file."
        echo ""
        echo "Autonomy levels (unchanged):"
        echo "  propose   plan and ask before changing the tree"
        echo "  execute   build, verify, seal a receipt, open a PR (default when no policy file)"
        echo "  operate   execute, and deploy only when you pass --deploy"
        echo ""
        echo "A .loki/policies.json or policies.yaml (repo, or LOKI_DATA_DIR, or ~/.loki) still decides"
        echo "allow, approval (draft PR) or deny."
        echo "When the contract cannot be satisfied the run stops with one question (BLOCKED)."
        echo "The summary still prints outcome, receipt, cost and the next command."
        echo ""
        echo "loki start keeps the provider model. Pass --haiku (or --cost lean) to try haiku for"
        echo "the first implement; a failed check climbs back. --max-cost stays the hard cap."
        return 0
    fi
    exec "$SKILL_DIR/bin/loki" start "$@"
}

# Demo mode - build a real project from a bundled PRD template
cmd_demo() {
    # Handle --help
    if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
        echo -e "${BOLD}loki demo${NC} - Build a real project from a bundled template"
        echo ""
        echo "Creates a temporary directory, copies the Simple Todo App PRD, and"
        echo "runs 'loki start' to build it end-to-end. Shows a summary when done"
        echo "and offers to open the result in a browser."
        echo ""
        echo "With --offline, instead replays a bundled real Evidence Receipt so"
        echo "you can see what Loki produces with no provider, no API key, no"
        echo "spend, and no network. This is a sample (a replay of a past build),"
        echo "not a verdict on your own code."
        echo ""
        echo "Usage: loki demo [options]"
        echo ""
        echo "Options:"
        echo "  --offline     Replay a bundled sample receipt (no key, no spend)"
        echo "  --dir PATH    Use a specific directory instead of a temp dir"
        echo "  --keep        Keep the temp directory on exit (default: kept)"
        echo "  --provider P  AI provider to use (default: claude)"
        echo "  --dry-run     Show what would happen without running"
        echo ""
        echo "Examples:"
        echo "  loki demo --offline              # Replay a sample receipt (no spend)"
        echo "  loki tour                        # Same as 'loki demo --offline'"
        echo "  loki tour --live                 # Tiny real task (loki wow); offline sample if no provider"
        echo "  loki wow                         # Live mini-task, or the offline sample with no key"
        echo "  loki demo                        # Build todo app in temp dir"
        echo "  loki demo --dir ~/demo-project   # Build in specific directory"
        echo "  loki demo --provider codex       # Use Codex instead of Claude"
        return 0
    fi

    # v7.30.0 (item 5): demo emits user-facing color (header, dry-run block,
    # cancel/refuse messages) on paths that always run even when piped/non-TTY,
    # so gate color on a TTY here (the global vars are only blanked on NO_COLOR).
    # Placed AFTER the --help early return so help output is untouched. Local
    # overrides only; mirrors provider-offer.sh / quickstart.sh.
    local BOLD="$BOLD" CYAN="$CYAN" YELLOW="$YELLOW" GREEN="$GREEN" DIM="$DIM" RED="$RED" NC="$NC"
    if [ ! -t 1 ]; then
        BOLD=''; CYAN=''; YELLOW=''; GREEN=''; DIM=''; RED=''; NC=''
    fi

    local version
    version=$(get_version)
    local demo_prd="$SKILL_DIR/templates/simple-todo-app.md"
    local demo_dir=""
    local provider=""
    local dry_run=false
    local offline=false
    local start_args=()
    # v7.29.0 (feature #4): demo now shows the FORCED-simple cost estimate
    # before spending and asks for confirmation. --yes / LOKI_ASSUME_YES skip
    # the prompt but still print the estimate (the spend is never hidden).
    local assume_yes=false
    if [ "${LOKI_ASSUME_YES:-}" = "1" ] || [ "${LOKI_ASSUME_YES:-}" = "true" ]; then
        assume_yes=true
    fi

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --yes|-y)
                assume_yes=true
                shift
                ;;
            --dir)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --dir requires a path${NC}"
                    return 1
                fi
                demo_dir="$2"
                shift 2
                ;;
            --dir=*)
                demo_dir="${1#*=}"
                shift
                ;;
            --provider)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --provider requires a value${NC}"
                    return 1
                fi
                provider="$2"
                shift 2
                ;;
            --provider=*)
                provider="${1#*=}"
                shift
                ;;
            --keep)
                # kept by default now, accepted for compat
                shift
                ;;
            --dry-run)
                dry_run=true
                shift
                ;;
            --offline|--tour)
                offline=true
                shift
                ;;
            *)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki demo --help' for usage."
                return 1
                ;;
        esac
    done

    # Offline taste path (feature #2): replay a bundled REAL Evidence Receipt
    # with NO provider, NO key, NO spend, NO network. This is the zero-friction
    # path a newcomer can run before installing or authenticating anything, so
    # it MUST run before (and bypass) the provider_offer_gate below. It never
    # builds, never calls cmd_start, and returns in well under 10s.
    #
    # Fake-green guardrail: the headline shown is the bundled receipt's REAL
    # honest headline (rendered by the same render_evidence_receipt_md the PR
    # path uses, which is never green unless genuinely VERIFIED). It is framed
    # as a SAMPLE/replay -- never presented as a verdict on the user's code.
    if [ "$offline" = true ]; then
        local sample_proof="$SKILL_DIR/skills/fixtures/demo-receipt/proof.json"
        if [ ! -f "$sample_proof" ]; then
            echo -e "${RED}Error: bundled sample receipt not found.${NC}" >&2
            echo "Expected at: $sample_proof" >&2
            return 1
        fi
        if ! declare -f render_evidence_receipt_md >/dev/null 2>&1; then
            echo -e "${RED}Error: receipt renderer unavailable.${NC}" >&2
            echo "Expected autonomy/lib/proof-pr.sh to be sourced." >&2
            return 1
        fi

        echo -e "${BOLD}Loki Mode v$version - Sample Receipt (offline replay)${NC}"
        echo ""
        echo -e "${YELLOW}This is a SAMPLE: a replay of a real past build, not a"
        echo -e "verdict on your code. No provider, no API key, no spend, no"
        echo -e "network were used to show it.${NC}"
        echo ""
        echo -e "${DIM}It is what Loki produces after a build: an Evidence Receipt"
        echo -e "with an honest headline you can recompute yourself.${NC}"
        echo ""

        # Print-only renderer; always returns 0. The headline label inside is
        # the receipt's genuine honesty.headline.
        render_evidence_receipt_md "$sample_proof" "" ""

        # The receipt above is the legacy proof sample. Below, the REAL verifier
        # (the same one 'loki verify' runs) checks a bundled sealed engine10
        # receipt offline against a bundled public key, then re-checks a copy
        # with one byte flipped. The verdict words come from the verifier's own
        # "verdict:" line; with no Bun the exact command is printed instead and
        # no verdict is claimed.
        local rcpt_dir="$SKILL_DIR/skills/fixtures/demo-receipt"
        local rcpt="$rcpt_dir/receipt.json"
        local rcpt_key="$rcpt_dir/pubkey.jwk"
        local rcpt_cmd="loki verify --pubkey $rcpt_key $rcpt"
        echo ""
        echo -e "${BOLD}Verify it yourself (offline, real verifier):${NC}"
        if [ ! -f "$rcpt" ] || [ ! -f "$rcpt_dir/events.jsonl" ] || [ ! -f "$rcpt_key" ]; then
            echo "UNCHECKED: the bundled sealed receipt, event log or public key is missing."
            echo "  Command: $rcpt_cmd"
        else
            local v_out v_verdict t_dir t_out t_verdict t_content
            v_out=$( ( _loki_bun_delegate engine10 verify --pubkey "$rcpt_key" "$rcpt" ) 2>&1 || true )
            v_verdict=$(printf '%s\n' "$v_out" | sed -n 's/^verdict: \([A-Z]*\).*/\1/p' | head -n 1)
            if [ -z "$v_verdict" ]; then
                echo "No verdict (the Bun runtime that runs the verifier is not available)."
                echo "  Install Bun (https://bun.sh), then run:"
                echo "  $rcpt_cmd"
            else
                echo "  $rcpt_cmd"
                echo "  Result: $v_verdict"
                if [ "$v_verdict" = "VERIFIED" ]; then
                    t_dir=$(mktemp -d "${TMPDIR:-/tmp}/loki-tour.XXXXXXXX" 2>/dev/null || true)
                    if [ -n "$t_dir" ] && [ -d "$t_dir" ]; then
                        cp "$rcpt_dir/events.jsonl" "$t_dir/events.jsonl"
                        t_content=$(cat "$rcpt")
                        # One byte: the first 'VERIFIED' in the receipt becomes 'VERIFIEE'.
                        printf '%s\n' "${t_content/VERIFIED/VERIFIEE}" > "$t_dir/receipt.json"
                        if [ "${t_content/VERIFIED/VERIFIEE}" = "$t_content" ]; then
                            echo "  (tamper check skipped: the receipt has no byte to flip)"
                            rm -f "$t_dir/receipt.json"
                        fi
                        t_out=$( ( _loki_bun_delegate engine10 verify --pubkey "$rcpt_key" "$t_dir/receipt.json" ) 2>&1 || true )
                        t_verdict=$(printf '%s\n' "$t_out" | sed -n 's/^verdict: \([A-Z]*\).*/\1/p' | head -n 1)
                        echo ""
                        echo "Now the same receipt with one byte changed (a copy; the bundled file is untouched):"
                        echo "  Result: ${t_verdict:-no verdict}"
                        case "$t_dir" in
                            "${TMPDIR:-/tmp}"/loki-tour.*) rm -rf "$t_dir" ;;
                        esac
                    fi
                fi
            fi
        fi

        echo ""
        echo -e "${DIM}---------------------------------------${NC}"
        echo -e "${BOLD}That was a sample. Build your own:${NC}"
        echo -e "  ${CYAN}loki quickstart${NC}                  # one guided command, your first real build"
        echo -e "  ${CYAN}loki demo${NC}                        # build a bundled sample app for real"
        return 0
    fi

    # v7.29.0: provider pre-flight gate (skipped for --dry-run, which never
    # spends). On a TTY, offer to install; on non-TTY/CI, print honest manual
    # instructions and exit 2 before any spend. Detect-first, so the nested
    # cmd_start gate below no-ops once a provider is present. --help already
    # returned at the top of cmd_demo.
    if [ "$dry_run" != true ] && declare -f provider_offer_gate >/dev/null 2>&1; then
        if ! provider_offer_gate; then
            exit 2
        fi
    fi

    # Fall back to examples/ if templates/ doesn't exist
    if [ ! -f "$demo_prd" ]; then
        demo_prd="$SKILL_DIR/examples/simple-todo-app.md"
    fi

    if [ ! -f "$demo_prd" ]; then
        echo -e "${RED}Error: Demo PRD not found${NC}"
        echo "Expected at: $SKILL_DIR/templates/simple-todo-app.md"
        return 1
    fi

    # Set up the demo directory
    if [ -z "$demo_dir" ]; then
        demo_dir=$(mktemp -d "${TMPDIR:-/tmp}/loki-demo-XXXXXX")
    else
        # Expand ~ if present
        demo_dir="${demo_dir/#\~/$HOME}"
        mkdir -p "$demo_dir"
    fi

    echo -e "${BOLD}Loki Mode v$version - Demo${NC}"
    echo ""
    echo -e "  PRD:       Simple Todo App"
    echo -e "  Directory: ${CYAN}$demo_dir${NC}"
    if [ -n "$provider" ]; then
        echo -e "  Provider:  $provider"
    else
        echo -e "  Provider:  claude (default)"
    fi
    echo ""

    # Copy PRD into demo directory
    cp "$demo_prd" "$demo_dir/prd.md"

    # Initialize git repo if not already one
    if [ ! -d "$demo_dir/.git" ]; then
        git -C "$demo_dir" init -q 2>/dev/null || true
    fi

    if [ "$dry_run" = true ]; then
        # v7.29.0: dry-run is now a genuine cost preview -- show the estimate,
        # no prompt, no spend.
        emit_demo_estimate "$demo_dir/prd.md" || true
        echo -e "${YELLOW}[dry-run] Would run:${NC}"
        echo "  cd $demo_dir"
        echo -n "  loki start prd.md --simple --yes"
        [ -n "$provider" ] && echo -n " --provider $provider"
        echo ""
        echo ""
        echo -e "${DIM}PRD copied to: $demo_dir/prd.md${NC}"
        return 0
    fi

    # v7.29.0 (feature #4): cost confirm before any spend. The estimate ALWAYS
    # prints (even with --yes), so the spend is never hidden. The prompt is
    # shown only on a TTY without --yes; non-TTY without --yes refuses (exit 2)
    # rather than hanging on read -- the project's "never hang in CI" idiom.
    local estimate_ok=true
    emit_demo_estimate "$demo_dir/prd.md" || estimate_ok=false

    # Interactive only when stdin is a real TTY and CI is not forcing
    # non-interactive (matches the project's first-run gate semantics).
    local demo_interactive=true
    if [ ! -t 0 ] || [ -n "${CI:-}" ]; then
        demo_interactive=false
    fi

    if [ "$assume_yes" != true ]; then
        if [ "$demo_interactive" = true ]; then
            local demo_answer=""
            if [ "$estimate_ok" = true ]; then
                # Default YES: Enter proceeds.
                echo -n "Build the Todo app now? [Y/n] "
                read -r demo_answer </dev/tty 2>/dev/null || demo_answer=""
                if [[ -n "$demo_answer" && ! "$demo_answer" =~ ^[Yy] ]]; then
                    echo ""
                    echo -e "Cancelled. Nothing was spent. The demo PRD is at:"
                    echo -e "  ${CYAN}$demo_dir/prd.md${NC}"
                    echo -e "Run 'loki plan ${demo_dir}/prd.md' for the full breakdown."
                    return 0
                fi
            else
                # No honest number available: default NO (the safe direction).
                echo -n "Proceed with the demo build anyway? [y/N] "
                read -r demo_answer </dev/tty 2>/dev/null || demo_answer=""
                if [[ ! "$demo_answer" =~ ^[Yy] ]]; then
                    echo ""
                    echo -e "Cancelled. Nothing was spent. The demo PRD is at:"
                    echo -e "  ${CYAN}$demo_dir/prd.md${NC}"
                    return 0
                fi
            fi
        else
            # Non-TTY without --yes: never hang; refuse with an honest message.
            echo "demo needs confirmation; re-run with --yes to proceed non-interactively" >&2
            return 2
        fi
    fi

    echo -e "${DIM}Starting autonomous build...${NC}"
    echo -e "${DIM}(Press Ctrl+C to stop at any time)${NC}"
    echo ""

    # Build start args
    start_args+=("prd.md" "--simple" "--yes")
    if [ -n "$provider" ]; then
        start_args+=("--provider" "$provider")
    fi

    # Run loki start from the demo directory
    local start_exit=0
    (cd "$demo_dir" && cmd_start "${start_args[@]}") || start_exit=$?

    echo ""
    echo -e "${DIM}---------------------------------------${NC}"
    echo -e "${BOLD}Demo Complete${NC}"
    echo -e "${DIM}---------------------------------------${NC}"
    echo ""

    # Show summary of what was generated
    echo -e "${BOLD}Project Directory:${NC} $demo_dir"
    echo ""

    if [ -d "$demo_dir" ]; then
        # Count generated files (exclude .git and .loki)
        local file_count
        file_count=$(find "$demo_dir" -type f \
            -not -path "$demo_dir/.git/*" \
            -not -path "$demo_dir/.loki/*" \
            -not -name "prd.md" \
            2>/dev/null | wc -l | tr -d ' ')

        local dir_count
        dir_count=$(find "$demo_dir" -type d \
            -not -path "$demo_dir/.git" \
            -not -path "$demo_dir/.git/*" \
            -not -path "$demo_dir/.loki" \
            -not -path "$demo_dir/.loki/*" \
            -not -path "$demo_dir" \
            2>/dev/null | wc -l | tr -d ' ')

        echo -e "${BOLD}Generated:${NC} $file_count files in $dir_count directories"
        echo ""

        # Show top-level structure
        if command -v tree &>/dev/null; then
            echo -e "${BOLD}Project Structure:${NC}"
            tree -L 2 --dirsfirst -I '.git|.loki' "$demo_dir" 2>/dev/null || \
                ls -1 "$demo_dir" 2>/dev/null
            echo ""
        else
            echo -e "${BOLD}Top-level contents:${NC}"
            ls -1 "$demo_dir" | grep -v '^\.\(git\|loki\)$' || true
            echo ""
        fi

        # Check for common web artifacts and offer to open
        local has_web=false
        local open_target=""

        # Check for common entry points
        for candidate in \
            "$demo_dir/index.html" \
            "$demo_dir/dist/index.html" \
            "$demo_dir/build/index.html" \
            "$demo_dir/public/index.html" \
            "$demo_dir/frontend/dist/index.html" \
            "$demo_dir/client/dist/index.html"; do
            if [ -f "$candidate" ]; then
                has_web=true
                open_target="$candidate"
                break
            fi
        done

        # Check for package.json with start script
        local has_start_script=false
        if [ -f "$demo_dir/package.json" ]; then
            if grep -q '"start"' "$demo_dir/package.json" 2>/dev/null; then
                has_start_script=true
            fi
            if grep -q '"dev"' "$demo_dir/package.json" 2>/dev/null; then
                has_start_script=true
            fi
        fi

        if [ "$has_web" = true ] && [ -n "$open_target" ]; then
            echo -e "${CYAN}A web frontend was generated.${NC}"
            echo ""
            local answer="n"
            # Ask only when a browser may open, so a test never blocks on /dev/tty.
            if loki_browser_allowed 2>/dev/null; then
                echo -n "Open in browser? [Y/n] "
                read -r answer </dev/tty 2>/dev/null || answer="n"
            fi
            if [[ -z "$answer" || "$answer" =~ ^[Yy] ]] && loki_open_url "$open_target" 2>/dev/null; then
                :
            else
                echo -e "Open this file in your browser: ${BLUE}$open_target${NC}"
            fi
        elif [ "$has_start_script" = true ]; then
            echo -e "${CYAN}A start script was found in package.json.${NC}"
            echo -e "To run the app:"
            echo -e "  cd $demo_dir && npm install && npm start"
        fi
    fi

    echo ""
    echo -e "${BOLD}Next steps:${NC}"
    echo -e "  cd $demo_dir                   # Explore the generated project"
    echo -e "  loki start ./your-prd.md       # Build your own project"
    echo -e "  loki init                      # Create a PRD interactively"

    if [ $start_exit -ne 0 ]; then
        echo ""
        echo -e "${YELLOW}Note: loki start exited with code $start_exit.${NC}"
        echo -e "${YELLOW}The demo directory is preserved at: $demo_dir${NC}"
    fi

    return $start_exit
}

# R7 (zero-config first run): set the lightweight TTFV execution profile.
# Shared by `cmd_quick` and the `loki start "<brief>"` brief sub-path so the
# fast first pass means the same thing in both: capped iterations, completion
# council off, simple complexity tier, heavy phases off. This is HONEST fast --
# it genuinely shortens the path to first visible value; it does not fake
# progress. Depth is opt-in via a plain `loki start` re-run.
# Usage: set_ttfv_lightweight_profile [max_iter]
set_ttfv_lightweight_profile() {
    local max_iter="${1:-${LOKI_MAX_ITERATIONS:-3}}"
    export LOKI_MAX_ITERATIONS="$max_iter"
    export LOKI_COMPLEXITY=simple
    export LOKI_COUNCIL_ENABLED=false
    export LOKI_PHASE_CODE_REVIEW=false
    export LOKI_PHASE_PERFORMANCE=false
    export LOKI_PHASE_ACCESSIBILITY=false
    export LOKI_PHASE_REGRESSION=false
    export LOKI_PHASE_UAT=false
    export LOKI_PHASE_WEB_RESEARCH=false
}

# R7: synthesize a forward-looking PRD from a one-line brief. Writes to a
# unique path (caller-provided) and echoes nothing -- the file is the output.
# Kept DISTINCT from .loki/generated-prd.md (codebase-analysis artifact) so it
# never pollutes the v7.8.1 generated-PRD-reuse signature logic. The brief text
# is the project intent; the rest is a minimal scaffold the agent fills in.
# Usage: synthesize_brief_prd <output_file> <brief_text>
synthesize_brief_prd() {
    local out_file="$1"
    local brief_text="$2"
    local out_dir
    out_dir="$(dirname "$out_file")"
    mkdir -p "$out_dir" 2>/dev/null || true
    cat > "$out_file" << BRIEFEOF
# Project Brief

## Overview
$brief_text

## Requirements
- Build the smallest working version of the above that a user can see and run.
- Prefer a runnable artifact (a page, a CLI, an API endpoint) over scaffolding.
- Follow conventional structure for the chosen stack; keep dependencies minimal.
- Write a short README describing how to run it.

## Success Criteria
- A user can run the result and observe the core behavior described above.
- No errors on a clean start; the happy path works end to end.

## Constraints
- This is a fast first pass (zero-config first run). Keep scope tight.
- Do not over-engineer; depth and hardening come on a later full run.
- No emojis, no em dashes in code, comments, or docs.

---

**Mode:** Brief (zero-config first run, lightweight first pass)
BRIEFEOF
}

# Build a bounded input for `loki start <repo-directory>`. This intentionally
# contains no caller-controlled path: repository identity comes from cwd, while
# the task remains stable, reviewable, and safe to render into a prompt.
#
# The trailing Mode line is the only variable part, and it must match what the
# run actually does: a fast pass runs with the council off, so a PRD telling the
# agent it is under "full verification" would be a false claim in the prompt
# itself. The body stays a quoted heredoc (no expansion, nothing injectable);
# the Mode line is appended from a fixed string chosen by a validated depth.
# Usage: synthesize_repo_prd <output_file> [full|fast]
synthesize_repo_prd() {
    local out_file="$1"
    local depth="${2:-full}"
    mkdir -p "$(dirname "$out_file")" 2>/dev/null || true
    cat > "$out_file" <<'REPOEOF'
# Existing Repository Improvement

## Outcome
Inspect this repository's instructions, product signals, and current tests.
Select one small, high-value user-facing improvement and implement it completely.

## Success Criteria
- The improvement has a clear user outcome and a bounded diff.
- Focused tests or checks prove the changed behavior.
- Existing unrelated work is preserved.
- Loki's normal outcome evidence identifies what changed and what passed.

## Constraints
- Make one coherent improvement, not a broad rewrite.
- Reuse the repository's established architecture and dependencies.
- Keep the change easy to review and roll back.
- Do not claim deployment, adoption, or customer value without evidence.
REPOEOF
    if [ "$depth" = "fast" ]; then
        printf '%s\n' \
            "- This is a fast, capped pass. Keep the change small enough to finish and prove." \
            "" \
            "**Mode:** Existing repository improvement (fast pass, reduced gates)." \
            "The completion council and heavy review phases are OFF on this run." \
            "Do not describe the result as council-verified or fully verified." >> "$out_file"
    else
        printf '\n**Mode:** Existing repository improvement (full verification)\n' >> "$out_file"
    fi
}

# Quick mode - lightweight single-task execution
cmd_quick() {
    # v7.6.3 B-11 fix: --help previously fell through to provider invocation
    # (task_desc="--help") which hung the CLI for 10+ seconds. Explicit guard.
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}Loki Mode -- quick lightweight execution${NC}"
            echo ""
            echo "Usage: loki quick \"description of what to do\""
            echo ""
            echo "Runs the Loki 10 engine on one small task (no PR is opened). Best for"
            echo "tiny tasks."
            echo "quick = small task with the verify loop. For a single pass with no loop, use loki edit."
            echo ""
            echo "Examples:"
            echo "  loki quick \"add dark mode to this React app\""
            echo "  loki quick \"fix the login bug in auth.js\""
            echo "  loki quick \"add input validation to the signup form\""
            echo "  loki quick \"write unit tests for the API endpoints\""
            echo ""
            echo "Options:"
            echo "  --help, -h       Show this help and exit"
            echo "  --verbose, -v    Show setup and progress output (same as LOKI_VERBOSE=1)"
            echo ""
            echo "Environment:"
            echo "  LOKI_MAX_ITERATIONS    Override the default 3-iteration cap"
            echo "  LOKI_VERBOSE=1         Show setup and progress output (default is quiet)"
            echo ""
            echo "See also: loki start (full RARV cycle), loki run (alias for start)"
            return 0
            ;;
    esac
    if [ $# -eq 0 ]; then
        echo -e "${RED}Error: No task description provided${NC}"
        echo ""
        echo "Usage: loki quick \"description of what to do\""
        echo ""
        echo "Examples:"
        echo "  loki quick \"add dark mode to this React app\""
        echo "  loki quick \"fix the login bug in auth.js\""
        echo "  loki quick \"add input validation to the signup form\""
        echo "  loki quick \"write unit tests for the API endpoints\""
        exit 64
    fi

    # #691: drop the unified config-file aliases from the task description. The
    # main() pre-pass already applied the file; without this, a trailing
    # `--config f.env` would be swept into task_desc (cmd_quick has no arg loop).
    local _quick_args=()
    local _skip_next=false
    local _qa _quick_task_seen=false
    for _qa in "$@"; do
        if [ "$_skip_next" = "true" ]; then
            _skip_next=false
            continue
        fi
        case "$_qa" in
            # A-134: --verbose/-v before the task text is a flag, never task text.
            --verbose|-v) if [ "$_quick_task_seen" = "false" ]; then export LOKI_VERBOSE=1; continue; fi ;;
            --config|--vars|--env-file)        _skip_next=true; continue ;;
            --config=*|--vars=*|--env-file=*)   continue ;;
            # LM-006: a leading unknown --flag is a usage error, never task text (a typo must not start a paid run).
            --?*) if [ "$_quick_task_seen" = "false" ] &&[[ "$_qa" != *" "* ]]; then
                    echo -e "${RED}Error: unknown option: $_qa${NC}" >&2
                    echo "Usage: loki quick [--verbose] \"description of what to do\"" >&2
                    exit 64
                 fi ;;
        esac
        _quick_task_seen=true
        _quick_args+=("$_qa")
    done
    set -- "${_quick_args[@]+"${_quick_args[@]}"}"
    if [ $# -eq 0 ]; then
        echo -e "${RED}Error: No task description provided${NC}"
        echo "Usage: loki quick [--verbose] \"description of what to do\""
        exit 64
    fi

    # v7.29.0: provider pre-flight gate. Offer install on a TTY; print honest
    # manual instructions and exit 2 on non-TTY/CI, before any spend. --help and
    # every usage error (no task, unknown option, exit 64) is decided above,
    # so a host with no provider still reports a typo as 64, not 2.
    if declare -f provider_offer_gate >/dev/null 2>&1; then
        if ! provider_offer_gate; then
            exit 2
        fi
    fi

    local task_desc="$*"
    local version=$(get_version)
    local max_iter="${LOKI_MAX_ITERATIONS:-3}"

    echo -e "${BOLD}Loki Mode v$version - Quick Mode${NC}"
    echo ""
    echo -e "${CYAN}Task:${NC} $task_desc"
    echo -e "${DIM}Running lightweight execution ($max_iter iterations max, no quality council)${NC}"
    echo ""

    # Create quick PRD from task description
    # BUG-PU-005: Use unique filename to prevent race conditions when
    # multiple simultaneous `loki quick` commands run in the same project
    mkdir -p "$LOKI_DIR"
    # Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
    # into run.sh, so these are never cleaned on exit and accumulate).
    find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
    find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
    local quick_prd="$LOKI_DIR/quick-prd-$$.md"
    cat > "$quick_prd" << QPRDEOF
# Quick Task

## Overview
$task_desc

## Requirements
- Complete the task described above
- Follow existing code patterns and conventions
- Write tests if applicable
- Do not break existing functionality

## Success Criteria
- Task is completed as described
- No errors or regressions introduced
- Code follows project conventions

## Constraints
- This is a quick single-task execution
- Keep changes minimal and focused
- Do not refactor unrelated code

---

**Mode:** Quick (lightweight, single-task)
QPRDEOF

    echo -e "${GREEN}Quick PRD generated${NC} at $quick_prd"
    echo ""

    # Set lightweight execution environment (shared TTFV profile -- see
    # set_ttfv_lightweight_profile; same profile the R7 brief sub-path uses).
    set_ttfv_lightweight_profile "$max_iter"

    # Record start for efficiency tracking
    record_session_start

    # Emit event
    emit_event session cli quick_start "task=$(echo "$task_desc" | head -c 100)"

    # Pre-flight: check that the provider CLI is installed
    local _quick_provider="${LOKI_PROVIDER:-claude}"
    if ! command -v "$_quick_provider" &>/dev/null; then
        echo -e "${RED}Error: Provider CLI '$_quick_provider' is not installed.${NC}"
        echo ""
        echo "Install your AI provider CLI first (same fix as loki doctor):"
        _loki_req_fix_line "$_quick_provider"
        echo ""
        echo "Then verify: loki doctor"
        exit 1
    fi

    # Run the orchestrator with quick settings (session leader -- see
    # _loki_new_session_exec; enables atomic group-kill on stop).
    _loki_new_session_exec "$RUN_SH" "$quick_prd"
}

# Docker Compose monitoring with auto-fix (v6.67.0)
cmd_monitor() {
    # A --help/-h in any position (e.g. `loki monitor --help`) short-circuits
    # to the help text BEFORE the Docker preconditions, so `monitor --help`
    # works even when Docker is down (same class as #574). The arg parser's
    # existing --help arm below prints the help; we just defer the Docker
    # checks until after parsing so help is reachable.
    local project_dir="."
    local watch_only=false
    local poll_interval="${LOKI_MONITOR_INTERVAL:-10}"
    local max_fixes="${LOKI_MONITOR_MAX_FIXES:-10}"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki monitor${NC} - AI-powered Docker Compose monitoring with auto-fix"
                echo ""
                echo "Usage: loki monitor [path] [options]"
                echo ""
                echo "Arguments:"
                echo "  path                  Project directory (default: current directory)"
                echo ""
                echo "Options:"
                echo "  --watch-only          Monitor only, do not auto-fix failures"
                echo "  --no-fix              Same as --watch-only"
                echo "  --interval SECONDS    Poll interval (default: 10, env: LOKI_MONITOR_INTERVAL)"
                echo "  --max-fixes N         Max fix attempts per session (default: 10, env: LOKI_MONITOR_MAX_FIXES)"
                echo "  --help, -h            Show this help"
                echo ""
                echo "The monitor polls Docker Compose services every N seconds."
                echo "When a service fails, it captures logs, feeds them to the AI"
                echo "provider (${LOKI_PROVIDER:-claude}), and lets the AI fix the code."
                echo "After fixing, it rebuilds and verifies the service."
                echo ""
                echo "Environment Variables:"
                echo "  LOKI_MONITOR_INTERVAL     Poll interval in seconds (default: 10)"
                echo "  LOKI_MONITOR_MAX_FIXES    Max fix attempts (default: 10)"
                echo "  LOKI_PROVIDER             AI provider for diagnosis (default: claude)"
                return 0
                ;;
            --watch-only|--no-fix)
                watch_only=true
                shift
                ;;
            --interval)
                poll_interval="${2:-10}"
                shift 2
                ;;
            --max-fixes)
                max_fixes="${2:-10}"
                shift 2
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki monitor --help' for usage."
                return 1
                ;;
            *)
                project_dir="$1"
                shift
                ;;
        esac
    done

    # Verify Docker is available (deferred past arg parsing so --help works
    # without a running daemon).
    if ! command -v docker &>/dev/null; then
        echo -e "${RED}Docker is not installed. Install from https://docker.com${NC}"
        return 1
    fi
    if ! docker info &>/dev/null 2>&1; then
        echo -e "${RED}Docker daemon is not running. Start Docker Desktop or the Docker service.${NC}"
        return 1
    fi

    # Resolve to absolute path
    if [[ ! "$project_dir" = /* ]]; then
        project_dir="$(cd "$project_dir" 2>/dev/null && pwd)" || {
            echo -e "${RED}Error: directory not found: $1${NC}"
            return 1
        }
    fi

    if [[ ! -f "$project_dir/docker-compose.yml" ]] && [[ ! -f "$project_dir/docker-compose.yaml" ]]; then
        echo -e "${RED}No docker-compose.yml found in $project_dir${NC}"
        return 1
    fi

    echo -e "${CYAN}AI-Powered Docker Monitor${NC}"
    echo -e "Project: ${BOLD}$project_dir${NC}"
    echo -e "Provider: ${GREEN}${LOKI_PROVIDER:-claude}${NC}"
    echo "Press Ctrl+C to stop."

    # Show what we're monitoring
    local initial_services
    initial_services=$(cd "$project_dir" && docker compose ps -a --format json 2>/dev/null | PARSE_MODE=names python3 -c "
import json, sys
raw = sys.stdin.read().strip()
if not raw: sys.exit(0)
try:
    parsed = json.loads(raw)
    services = parsed if isinstance(parsed, list) else [parsed]
except json.JSONDecodeError:
    services = []
    for line in raw.split(chr(10)):
        try: services.append(json.loads(line))
        except: pass
names = [s.get('Service', s.get('Name','?')) for s in services if isinstance(s, dict)]
print(', '.join(names))
" 2>/dev/null)

    if [[ -n "$initial_services" ]]; then
        echo -e "Services: ${GREEN}$initial_services${NC}"
    else
        echo -e "${YELLOW}No running services detected yet. Waiting for docker compose up...${NC}"
    fi

    if [[ "$watch_only" == "true" ]]; then
        echo -e "Mode: ${YELLOW}watch-only${NC} (auto-fix disabled)"
    else
        echo -e "Mode: ${GREEN}auto-fix enabled${NC} (max $max_fixes attempts)"
    fi
    echo ""

    local fix_count=0
    local consecutive_healthy=0

    trap 'echo ""; echo -e "${CYAN}Monitor stopped.${NC}"; return 0' INT TERM

    while true; do
        # 1. CAPTURE: Get service status (all services including stopped)
        local ps_output
        ps_output=$(cd "$project_dir" && docker compose ps -a --format json 2>/dev/null)

        if [[ -z "$ps_output" ]]; then
            echo -e "${YELLOW}No services found. Waiting...${NC}"
            sleep "$poll_interval"
            continue
        fi

        # Parse services and find failures
        local failed_services=""
        local all_healthy=true
        local service_summary=""

        while IFS= read -r line; do
            [[ -z "$line" ]] && continue
            local svc_name svc_state svc_exit
            svc_name=$(echo "$line" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('Service',d.get('Name','')))" 2>/dev/null || echo "unknown")
            svc_state=$(echo "$line" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('State','unknown'))" 2>/dev/null || echo "unknown")
            svc_exit=$(echo "$line" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('ExitCode',0))" 2>/dev/null || echo "0")

            service_summary="${service_summary}${svc_name}=${svc_state} "

            if [[ "$svc_state" == "exited" ]] || [[ "$svc_state" == "dead" ]]; then
                all_healthy=false
                failed_services="${failed_services} ${svc_name}"
            fi
        done <<< "$ps_output"

        if [[ "$all_healthy" == "true" ]]; then
            consecutive_healthy=$((consecutive_healthy + 1))
            printf "\r${GREEN}[healthy]${NC} %s (%s)   " "$service_summary" "$(date +%H:%M:%S)"
            sleep "$poll_interval"
            continue
        fi

        consecutive_healthy=0

        # 2. CAPTURE LOGS and CONTEXT for each failed service, then feed to AI
        for svc_name in $failed_services; do
            if [[ $fix_count -ge $max_fixes ]]; then
                echo -e "\n${RED}Max fix attempts ($max_fixes) reached for this session.${NC}"
                echo "Manual intervention needed. Check: cd $project_dir && docker compose logs $svc_name"
                sleep "$poll_interval"
                continue 2
            fi

            if [[ "$watch_only" == "true" ]]; then
                echo -e "${YELLOW}[WATCH] $svc_name is down. Fix disabled (--watch-only mode).${NC}"
                echo -e "  Logs: cd $project_dir && docker compose logs $svc_name"
            elif [[ $fix_count -lt $max_fixes ]]; then
                echo -e "\n${RED}[FAILURE] $svc_name is down${NC}"

                local logs
                logs=$(cd "$project_dir" && docker compose logs --tail 50 "$svc_name" 2>/dev/null | head -c 3000)

                # Gather docker-compose.yml for AI context (truncated to avoid ARG_MAX)
                local compose_content=""
                if [[ -f "$project_dir/docker-compose.yml" ]]; then
                    compose_content=$(head -c 5000 "$project_dir/docker-compose.yml")
                elif [[ -f "$project_dir/docker-compose.yaml" ]]; then
                    compose_content=$(head -c 5000 "$project_dir/docker-compose.yaml")
                fi

                # Gather Dockerfile for the failing service (truncated)
                local dockerfile_content=""
                for df_path in "$project_dir/$svc_name/Dockerfile" "$project_dir/Dockerfile" "$project_dir/Dockerfile.$svc_name"; do
                    if [[ -f "$df_path" ]]; then
                        dockerfile_content=$(head -c 3000 "$df_path")
                        break
                    fi
                done

                # 3. FEED TO AI: Construct a rich prompt with all context
                local ai_prompt="You are debugging a Docker Compose service that has failed.

SERVICE: $svc_name
STATUS: exited/dead

DOCKER COMPOSE LOGS (last 50 lines):
$logs

DOCKER-COMPOSE.YML:
$compose_content"

                if [[ -n "$dockerfile_content" ]]; then
                    ai_prompt="${ai_prompt}

DOCKERFILE ($svc_name/Dockerfile):
$dockerfile_content"
                fi

                ai_prompt="${ai_prompt}

INSTRUCTIONS:
1. Analyze the error in the logs above
2. Identify the root cause
3. Fix the issue by editing the necessary files (docker-compose.yml, Dockerfile, source code, package.json, requirements.txt, etc.)
4. Make sure the fix works on any platform (Docker Desktop, Linux, Docker-in-Docker, Kubernetes)
5. Do NOT just restart -- actually fix the underlying code/config problem
6. After fixing, the system will rebuild with 'docker compose up --build'
7. Common issues: named volumes for node_modules (use anonymous), missing dependencies, port conflicts, wrong commands"

                echo -e "${CYAN}[AI] Analyzing $svc_name failure with ${LOKI_PROVIDER:-claude}...${NC}"

                # 4. LET AI FIX: Run loki quick with the AI prompt
                # The AI provider (claude/codex/ollama) decides what to fix
                fix_count=$((fix_count + 1))
                echo -e "${CYAN}[FIX] Attempt $fix_count/$max_fixes${NC}"

                (
                    cd "$project_dir"
                    LOKI_MAX_ITERATIONS=5 LOKI_AUTO_FIX=true \
                    "$0" quick "$ai_prompt" 2>&1 | while IFS= read -r fline; do
                        echo "  $fline"
                    done
                )

                # 5. REBUILD: Docker compose up --build for the fixed service
                echo -e "${CYAN}[REBUILD] Rebuilding $svc_name...${NC}"
                (cd "$project_dir" && docker compose up -d --build --no-deps "$svc_name" 2>&1) | while IFS= read -r fline; do
                    echo "  $fline"
                done

                # 6. VERIFY: Wait and check if the fix worked
                echo -e "${CYAN}[VERIFY] Waiting 15s for $svc_name to stabilize...${NC}"
                sleep 15

                local verify_state
                verify_state=$(cd "$project_dir" && docker compose ps -a --format json 2>/dev/null | VERIFY_SVC="$svc_name" python3 -c "
import json, sys, os
svc = os.environ['VERIFY_SVC']
raw = sys.stdin.read().strip()
if not raw:
    print('unknown')
    sys.exit(0)
# Handle both JSON array (v2.21+) and NDJSON formats
try:
    parsed = json.loads(raw)
    services = parsed if isinstance(parsed, list) else [parsed]
except json.JSONDecodeError:
    services = []
    for line in raw.split(chr(10)):
        try: services.append(json.loads(line))
        except: pass
for s in services:
    if not isinstance(s, dict): continue
    if s.get('Service', s.get('Name','')) == svc:
        print(s.get('State','unknown'))
        sys.exit(0)
print('unknown')
" 2>/dev/null || echo "unknown")

                if [[ "$verify_state" == "running" ]]; then
                    echo -e "${GREEN}[SUCCESS] $svc_name is now running!${NC}"
                else
                    echo -e "${YELLOW}[RETRY] $svc_name still not healthy (state: $verify_state). Will retry on next poll.${NC}"
                fi
            fi
        done

        sleep "$poll_interval"
    done
}

# Project scaffolding (v6.28.0)
cmd_init() {
    # Guard: check if .loki/ already exists to avoid overwriting active session
    if [[ -d ".loki" ]]; then
        if [[ -f ".loki/loki.pid" ]]; then
            local pid
            pid=$(cat ".loki/loki.pid" 2>/dev/null)
            if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
                echo -e "${RED}Cannot initialize: active session running. Stop it first with 'loki stop'.${NC}"
                return 1
            fi
        fi
        # stderr, not stdout: `loki init --json` writes a machine-readable
        # template list to stdout, and this human-facing banner was landing in
        # the middle of it -- the second `init` in a directory emitted invalid
        # JSON, so any tool parsing it broke on reinit but worked on first run.
        # Diagnostics belong on stderr; stdout is the data channel.
        echo -e "${YELLOW}Reinitializing existing .loki/ directory${NC}" >&2
    fi

    local version=$(get_version)
    local project_name=""
    local template_name=""
    local no_git=false
    local stdout_mode=false
    local dry_run=false
    local list_mode=false
    local json_mode=false

    # 21 built-in template names (order: simple, standard, complex)
    local TEMPLATE_NAMES=(
        simple-todo-app
        static-landing-page
        api-only
        rest-api
        rest-api-auth
        cli-tool
        discord-bot
        chrome-extension
        blog-platform
        full-stack-demo
        web-scraper
        data-pipeline
        dashboard
        game
        slack-bot
        npm-library
        microservice
        mobile-app
        saas-starter
        e-commerce
        ai-chatbot
    )

    # Template label lookup function (bash 3.2 compatible - no associative arrays)
    _get_template_label() {
        case "$1" in
            simple-todo-app) echo "Simple Todo App" ;;
            static-landing-page) echo "Static Landing Page" ;;
            api-only) echo "REST API (No Frontend)" ;;
            rest-api) echo "REST API" ;;
            rest-api-auth) echo "REST API with Auth" ;;
            cli-tool) echo "CLI Tool" ;;
            discord-bot) echo "Discord Bot" ;;
            chrome-extension) echo "Chrome Extension" ;;
            blog-platform) echo "Blog Platform" ;;
            full-stack-demo) echo "Full-Stack Demo" ;;
            web-scraper) echo "Web Scraper" ;;
            data-pipeline) echo "Data Pipeline" ;;
            dashboard) echo "Analytics Dashboard" ;;
            game) echo "Browser Game" ;;
            slack-bot) echo "Slack Bot" ;;
            npm-library) echo "npm Library" ;;
            microservice) echo "Microservice" ;;
            mobile-app) echo "Mobile App" ;;
            saas-starter) echo "SaaS Starter Kit" ;;
            e-commerce) echo "E-Commerce Store" ;;
            ai-chatbot) echo "AI Chatbot (RAG)" ;;
            *) echo "$1" ;;
        esac
    }

    local template_count=${#TEMPLATE_NAMES[@]}

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --list)
                list_mode=true
                shift
                ;;
            --json)
                json_mode=true
                list_mode=true
                shift
                ;;
            --template|-t)
                if [[ -n "${2:-}" ]]; then
                    template_name="$2"
                    shift 2
                else
                    echo -e "${RED}--template requires a value${NC}"
                    exit 1
                fi
                ;;
            --no-git)
                no_git=true
                shift
                ;;
            --stdout)
                stdout_mode=true
                shift
                ;;
            --dry-run)
                dry_run=true
                shift
                ;;
            --help|-h)
                echo -e "${BOLD}loki init${NC} - Project scaffolding with PRD templates"
                echo ""
                echo "Usage: loki init [project-name] [--template TYPE]"
                echo ""
                echo "Creates a new project scaffold with a PRD, config directory, and README."
                echo "If no project-name is given, scaffolds the current directory."
                echo ""
                echo "What it creates:"
                echo "  prd.md                   PRD from selected template"
                echo "  .loki/loki.config.json   Project configuration"
                echo "  README.md                Project README stub"
                echo "  .gitignore               Git ignore rules (with git init)"
                echo ""
                echo "Options:"
                echo "  --template, -t TYPE     Template name (e.g., saas-starter, cli-tool)"
                echo "  --no-git                Skip git init"
                echo "  --stdout                Print PRD to stdout instead of writing files"
                echo "  --list                  List all available templates"
                echo "  --json                  Machine-readable template list (JSON)"
                echo "  --dry-run               Show what would be created without doing it"
                echo "  --help, -h              Show this help"
                echo ""
                echo "Examples:"
                echo "  loki init my-saas --template saas-starter    Create my-saas/ with SaaS PRD"
                echo "  loki init --template cli-tool            Scaffold current dir with CLI PRD"
                echo "  loki init my-app                         Create my-app/ (prompts for template)"
                echo "  loki init --list                         Show all $template_count templates"
                echo "  loki init --template rest-api --stdout   Print REST API PRD to stdout"
                echo "  loki init my-app --dry-run               Preview without writing anything"
                exit 0
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki init --help' for usage."
                exit 1
                ;;
            *)
                if [[ -z "$project_name" ]]; then
                    project_name="$1"
                else
                    echo -e "${RED}Too many arguments${NC}"
                    echo "Usage: loki init [project-name] [--template TYPE]"
                    exit 1
                fi
                shift
                ;;
        esac
    done

    # Handle --list / --json
    if $list_mode; then
        if $json_mode; then
            echo "["
            local first=true
            for tname in "${TEMPLATE_NAMES[@]}"; do
                if $first; then
                    first=false
                else
                    echo ","
                fi
                printf '  {"name": "%s", "label": "%s"}' "$tname" "$(_get_template_label "$tname")"
            done
            echo ""
            echo "]"
        else
            echo -e "${BOLD}Available PRD Templates ($template_count)${NC}"
            echo ""
            echo -e "  ${DIM}Simple:${NC}"
            local idx=1
            for tname in "${TEMPLATE_NAMES[@]}"; do
                if [[ "$tname" == "rest-api" ]]; then
                    echo -e "  ${DIM}Standard:${NC}"
                elif [[ "$tname" == "mobile-app" ]]; then
                    echo -e "  ${DIM}Complex:${NC}"
                fi
                printf "  %2d. ${CYAN}%-22s${NC} %s\n" "$idx" "$tname" "$(_get_template_label "$tname")"
                idx=$((idx + 1))
            done
            echo ""
            echo "Usage: loki init [project-name] --template <name>"
        fi
        exit 0
    fi

    # Interactive template picker if no template specified
    if [[ -z "$template_name" ]]; then
        echo -e "${BOLD}Loki Mode v$version - Project Scaffolding${NC}"
        echo ""
        echo "Select a PRD template:"
        echo ""
        echo -e "  ${DIM}Simple:${NC}"
        local idx=1
        for tname in "${TEMPLATE_NAMES[@]}"; do
            if [[ "$tname" == "rest-api" ]]; then
                echo -e "  ${DIM}Standard:${NC}"
            elif [[ "$tname" == "mobile-app" ]]; then
                echo -e "  ${DIM}Complex:${NC}"
            fi
            printf "  %2d. %-22s %s\n" "$idx" "$tname" "$(_get_template_label "$tname")"
            idx=$((idx + 1))
        done
        echo ""
        echo -e -n "${BOLD}Choose [1-$template_count]:${NC} "
        read -r choice

        if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "$template_count" ]; then
            echo -e "${RED}Invalid choice. Enter a number between 1 and $template_count.${NC}"
            exit 1
        fi

        template_name="${TEMPLATE_NAMES[$((choice - 1))]}"
        echo ""
        echo -e "Selected: ${CYAN}$template_name${NC} ($(_get_template_label "$template_name"))"
    fi

    # R10: a hub-installed template (.loki/templates/<name>.md) is also valid.
    # Resolve it via agents/hub_install.py (validates the name as a safe id).
    local _installed_tpl=""
    local _hub_py_init="$SKILL_DIR/agents/hub_install.py"
    [ -f "$_hub_py_init" ] || _hub_py_init="agents/hub_install.py"
    if [[ -f "$_hub_py_init" ]]; then
        _installed_tpl=$(TPL_NAME="$template_name" HUB_PY="$_hub_py_init" python3 -c "
import os
import importlib.util as ilu
sp = ilu.spec_from_file_location('loki_hub_install', os.environ.get('HUB_PY'))
m = ilu.module_from_spec(sp); sp.loader.exec_module(m)
p = m.installed_template_path(os.environ['TPL_NAME'])
print(p or '')
" 2>/dev/null)
    fi

    # Validate template_name against known list before filesystem lookup
    local _tpl_valid=false
    for _tpl_check in "${TEMPLATE_NAMES[@]}"; do
        if [[ "$_tpl_check" == "$template_name" ]]; then
            _tpl_valid=true
            break
        fi
    done
    if [[ -n "$_installed_tpl" ]]; then
        _tpl_valid=true
    fi
    if ! $_tpl_valid; then
        echo -e "${RED}Unknown template: $template_name${NC}"
        echo ""
        echo "Available templates:"
        for tname in "${TEMPLATE_NAMES[@]}"; do
            echo "  $tname"
        done
        echo ""
        echo "Run 'loki init --list' for built-in details, 'loki template list' for installed."
        exit 1
    fi

    # Resolve template file
    local template_file=""
    if [[ -n "$_installed_tpl" && -f "$_installed_tpl" ]]; then
        template_file="$_installed_tpl"
    fi
    if [[ -z "$template_file" ]]; then
        for dir in "$SKILL_DIR/templates" "$SKILL_DIR/examples"; do
            if [[ -f "$dir/${template_name}.md" ]]; then
                template_file="$dir/${template_name}.md"
                break
            fi
        done
    fi

    if [[ -z "$template_file" ]]; then
        echo -e "${RED}Unknown template: $template_name${NC}"
        echo ""
        echo "Available templates:"
        for tname in "${TEMPLATE_NAMES[@]}"; do
            echo "  $tname"
        done
        echo ""
        echo "Run 'loki init --list' for details."
        exit 1
    fi

    # Read template content
    local prd_content
    prd_content=$(cat "$template_file")

    # --stdout mode: just print PRD and exit
    if $stdout_mode; then
        echo "$prd_content"
        exit 0
    fi

    # Determine target directory
    local target_dir
    if [[ -n "$project_name" ]]; then
        target_dir="$(pwd)/$project_name"
    else
        target_dir="$(pwd)"
    fi

    # Guard: check if target directory has an active .loki/ session
    if [[ -n "$project_name" ]] && [[ -d "$target_dir/.loki" ]]; then
        if [[ -f "$target_dir/.loki/loki.pid" ]]; then
            local _guard_pid
            _guard_pid=$(cat "$target_dir/.loki/loki.pid" 2>/dev/null)
            if [[ -n "$_guard_pid" ]] && kill -0 "$_guard_pid" 2>/dev/null; then
                echo -e "${RED}Cannot initialize: active session running in $target_dir. Stop it first.${NC}"
                return 1
            fi
        fi
        # stderr for the same reason as the sibling banner above: stdout is the
        # machine-readable channel. Fixing only the path the failing test
        # exercised would leave this one to resurface the same bug.
        echo -e "${YELLOW}Reinitializing existing .loki/ directory in $target_dir${NC}" >&2
    fi

    # Build config JSON
    local config_json
    config_json=$(cat <<ENDJSON
{
  "version": "$version",
  "template": "$template_name",
  "created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "provider": "claude",
  "complexity": "auto",
  "quality_gates": true,
  "parallel_mode": false,
  "dashboard": true
}
ENDJSON
)

    # Build README content
    local display_name="${project_name:-$(basename "$target_dir")}"
    local readme_content
    readme_content=$(cat <<ENDREADME
# $display_name

Built with [Loki Mode](https://www.autonomi.dev/) using the \`$template_name\` template.

## Getting Started

\`\`\`bash
# Review the PRD
cat prd.md

# Launch autonomous development
claude --dangerously-skip-permissions
# Then: "Loki Mode with PRD at prd.md"

# Or via CLI
loki start prd.md
\`\`\`

## Project Structure

This project was scaffolded by \`loki init\`. Key files:

- \`prd.md\` - Product Requirements Document
- \`.loki/loki.config.json\` - Loki Mode configuration
ENDREADME
)

    # Build gitignore content
    local gitignore_content
    gitignore_content=$(cat <<'ENDGITIGNORE'
# Dependencies
node_modules/
venv/
__pycache__/

# Build output
dist/
build/
*.egg-info/

# Loki Mode runtime state (keep .loki/loki.config.json)
.loki/state/
.loki/memory/episodic/
.loki/metrics/
.loki/queue/
.loki/sessions/
.loki/logs/

# Environment
.env
.env.local

# OS
.DS_Store
Thumbs.db
ENDGITIGNORE
)

    # --dry-run mode: show what would be created
    if $dry_run; then
        echo -e "${BOLD}Dry run: loki init${NC}"
        echo ""
        if [[ -n "$project_name" ]]; then
            echo -e "  ${GREEN}CREATE${NC} $target_dir/"
        fi
        echo -e "  ${GREEN}CREATE${NC} $target_dir/prd.md ($(_get_template_label "$template_name") template)"
        echo -e "  ${GREEN}CREATE${NC} $target_dir/.loki/"
        echo -e "  ${GREEN}CREATE${NC} $target_dir/.loki/loki.config.json"
        echo -e "  ${GREEN}CREATE${NC} $target_dir/README.md"
        if ! $no_git; then
            if [[ -n "$project_name" ]] || ! git rev-parse --git-dir &>/dev/null; then
                echo -e "  ${GREEN}CREATE${NC} $target_dir/.gitignore"
                echo -e "  ${GREEN}RUN${NC}    git init"
            fi
        fi
        echo ""
        echo "Run without --dry-run to create these files."
        exit 0
    fi

    # Create project directory if project name given
    if [[ -n "$project_name" ]]; then
        if [[ -d "$target_dir" ]]; then
            echo -e "${RED}Directory already exists: $target_dir${NC}"
            exit 1
        fi
        if ! mkdir -p "$target_dir"; then
            echo -e "${RED}Failed to create directory: $target_dir${NC}"
            exit 1
        fi
    fi

    # Create .loki config directory
    if ! mkdir -p "$target_dir/.loki"; then
        echo -e "${RED}Failed to create .loki directory in: $target_dir${NC}"
        exit 1
    fi

    # Write files
    echo "$prd_content" > "$target_dir/prd.md"
    echo "$config_json" > "$target_dir/.loki/loki.config.json"

    # Only write README if it does not already exist
    local did_write_readme=false
    if [[ ! -f "$target_dir/README.md" ]]; then
        echo "$readme_content" > "$target_dir/README.md"
        did_write_readme=true
    fi

    # Git init (unless --no-git or already in a git repo)
    local did_git_init=false
    if ! $no_git; then
        if [[ -n "$project_name" ]] || ! git rev-parse --git-dir &>/dev/null; then
            echo "$gitignore_content" > "$target_dir/.gitignore"
            (cd "$target_dir" && git init -q)
            did_git_init=true
        fi
    fi

    # Print summary
    echo ""
    echo -e "${GREEN}Project scaffolded:${NC} $target_dir"
    echo ""
    echo "  Files created:"
    echo -e "    prd.md                   ${DIM}$(_get_template_label "$template_name") template${NC}"
    echo -e "    .loki/loki.config.json   ${DIM}project configuration${NC}"
    if $did_write_readme; then
        echo -e "    README.md                ${DIM}project readme${NC}"
    fi
    if $did_git_init; then
        echo -e "    .gitignore               ${DIM}git ignore rules${NC}"
        echo -e "    ${DIM}git repo initialized${NC}"
    fi
    echo ""
    echo "Next steps:"
    if [[ -n "$project_name" ]]; then
        echo -e "  1. ${BOLD}cd $project_name${NC}"
        echo -e "  2. Review and edit: ${BOLD}prd.md${NC}"
        echo -e "  3. Run: ${BOLD}loki start prd.md${NC}"
    else
        echo -e "  1. Review and edit: ${BOLD}prd.md${NC}"
        echo -e "  2. Run: ${BOLD}loki start prd.md${NC}"
    fi

    # Check if an AI provider CLI is available
    local _has_provider=false
    # Must match auto_detect_provider() in providers/loader.sh. Omitting
    # opencode here made `loki provider list` under-report a provider the
    # runner will happily select -- the same drift that blocked
    # opencode-only machines entirely until v8.76.0.
    for _pcli in claude cline codex aider opencode; do
        if command -v "$_pcli" &>/dev/null; then
            _has_provider=true
            break
        fi
    done
    if ! $_has_provider; then
        echo ""
        echo -e "${YELLOW}Note: No AI provider CLI detected.${NC}"
        echo "  Install at least one before running 'loki start':"
        echo "    npm install -g @anthropic-ai/claude-code   (recommended)"
        echo "    npm install -g @openai/codex"
        echo ""
        echo "  Then verify your setup: ${BOLD}loki doctor${NC}"
    fi
}

# Dogfooding statistics
cmd_dogfood() {
    # Handle --help
    if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
        echo -e "${BOLD}loki dogfood${NC} - Show self-development statistics"
        echo ""
        echo "Shows what percentage of loki-mode code was written by loki-mode itself."
        echo ""
        echo "Usage: loki dogfood [--json]"
        echo ""
        echo "Options:"
        echo "  --json    Output in JSON format"
        return 0
    fi

    local json_flag=""
    if [[ "${1:-}" == "--json" ]]; then
        json_flag="--json"
    fi

    # Honest degradation: scripts/dogfood-stats.sh is a development-only helper
    # that is NOT shipped in the npm tarball (package.json `files` omits
    # scripts/). On a packaged install it is absent, so report that plainly
    # rather than a bare "not found" that reads like a bug. Message goes to
    # STDERR so a --json caller's stdout stays clean; --json gets a structured
    # degraded payload on stdout so consumers do not choke on prose.
    local stats_script="$SKILL_DIR/scripts/dogfood-stats.sh"
    if [ ! -f "$stats_script" ]; then
        if [ -n "$json_flag" ]; then
            printf '{"available": false, "reason": "dogfood-stats.sh is a dev-only helper not shipped in the published package"}\n'
        else
            echo "loki report dogfood: self-development stats are unavailable in this install." >&2
            echo "The dogfood-stats.sh helper is a development-only script and is not" >&2
            echo "shipped in the published package. Run it from a loki-mode source checkout." >&2
        fi
        return 0
    fi

    bash "$stats_script" $json_flag
}

# List available templates (legacy helper, kept for backward compat)
_list_templates() {
    cmd_init --list
}

# Watchdog: process health monitoring
cmd_watchdog() {
    local subcommand="${1:-status}"
    shift 2>/dev/null || true

    case "$subcommand" in
        status)
            # Show watchdog state
            if [[ "${LOKI_WATCHDOG:-false}" == "true" ]]; then
                echo -e "${GREEN}Watchdog: ENABLED${NC} (interval: ${LOKI_WATCHDOG_INTERVAL:-30}s)"
            else
                echo "Watchdog: DISABLED"
                echo "Enable with: LOKI_WATCHDOG=true loki start"
            fi

            echo ""
            echo -e "${BOLD}Process Health:${NC}"

            # Check dashboard
            local dpid_file="$LOKI_DIR/dashboard/dashboard.pid"
            if [[ -f "$dpid_file" ]]; then
                local dpid
                dpid=$(cat "$dpid_file" 2>/dev/null)
                if [[ -n "$dpid" ]] && kill -0 "$dpid" 2>/dev/null; then
                    echo -e "  Dashboard (PID $dpid): ${GREEN}alive${NC}"
                else
                    echo -e "  Dashboard (PID ${dpid:-?}): ${RED}DEAD${NC}"
                fi
            else
                echo -e "  Dashboard: ${DIM}not running${NC}"
            fi

            # Check session
            local spid_file="$LOKI_DIR/loki.pid"
            if [[ -f "$spid_file" ]]; then
                local spid
                spid=$(cat "$spid_file" 2>/dev/null)
                if [[ -n "$spid" ]] && kill -0 "$spid" 2>/dev/null; then
                    echo -e "  Session  (PID $spid): ${GREEN}alive${NC}"
                else
                    echo -e "  Session  (PID ${spid:-?}): ${RED}DEAD${NC}"
                fi
            else
                echo -e "  Session:  ${DIM}not running${NC}"
            fi

            # Check agents
            local agents_file="$LOKI_DIR/state/agents.json"
            if [[ -f "$agents_file" ]]; then
                local agent_info
                agent_info=$(python3 -c "
import json, os
try:
    agents = json.load(open('$agents_file'))
    alive = dead = other = 0
    for a in agents:
        pid = a.get('pid')
        status = a.get('status', '')
        if status in ('terminated', 'completed', 'failed', 'crashed'):
            other += 1
            continue
        if pid:
            try:
                os.kill(int(pid), 0)
                alive += 1
            except (OSError, ValueError):
                dead += 1
        else:
            other += 1
    print(f'{alive}:{dead}:{other}')
except Exception:
    print('0:0:0')
" 2>/dev/null || echo "0:0:0")
                local a_alive a_dead a_other
                IFS=: read -r a_alive a_dead a_other <<< "$agent_info"
                if [[ "$a_alive" -gt 0 ]] || [[ "$a_dead" -gt 0 ]]; then
                    echo -e "  Agents:   ${GREEN}${a_alive} alive${NC}, ${RED}${a_dead} dead${NC}, ${DIM}${a_other} finished${NC}"
                elif [[ "$a_other" -gt 0 ]]; then
                    echo -e "  Agents:   ${DIM}${a_other} finished${NC}"
                else
                    echo -e "  Agents:   ${DIM}none${NC}"
                fi
            else
                echo -e "  Agents:   ${DIM}none${NC}"
            fi
            ;;
        help)
            echo "Usage: loki watchdog [status|help]"
            echo ""
            echo "Monitor process health for Loki Mode sessions."
            echo ""
            echo "Subcommands:"
            echo "  status    Show health of dashboard, session, and agent processes (default)"
            echo "  help      Show this help"
            echo ""
            echo "Environment:"
            echo "  LOKI_WATCHDOG=true          Enable watchdog monitoring during sessions"
            echo "  LOKI_WATCHDOG_INTERVAL=30   Check interval in seconds (default: 30)"
            ;;
        *)
            echo -e "${RED}Unknown watchdog command: $subcommand${NC}"
            cmd_watchdog help
            return 1
            ;;
    esac
}

# Prompt optimization via dashboard API
cmd_optimize() {
    local sessions=10
    local dry_run=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --sessions) if [[ -z "${2:-}" ]]; then echo -e "${RED}Error: --sessions requires a value${NC}"; exit 1; fi; sessions="$2"; shift 2 ;;
            --sessions=*) sessions="${1#*=}"; shift ;;
            --dry-run) dry_run="true"; shift ;;
            --help|-h)
                echo -e "${BOLD}loki optimize${NC} - Optimize prompts based on session history"
                echo ""
                echo "Usage: loki optimize [options]"
                echo ""
                echo "Analyzes recent session failures and proposes prompt improvements."
                echo "Requires the dashboard API to be running (loki serve)."
                echo ""
                echo "Options:"
                echo "  --sessions N   Number of recent sessions to analyze (default: 10)"
                echo "  --dry-run      Show proposed changes without applying"
                echo ""
                echo "Examples:"
                echo "  loki optimize                  # Optimize using last 10 sessions"
                echo "  loki optimize --sessions 20    # Analyze last 20 sessions"
                echo "  loki optimize --dry-run        # Preview changes only"
                exit 0
                ;;
            *)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki optimize --help' for usage."
                exit 1
                ;;
        esac
    done

    local port="${LOKI_DASHBOARD_PORT:-57374}"
    local host="127.0.0.1"
    local dry_run_param="false"
    if [ -n "$dry_run" ]; then
        dry_run_param="true"
    fi
    local url="http://${host}:${port}/api/prompt-optimize?sessions=${sessions}&dry_run=${dry_run_param}"

    echo -e "${BOLD}Analyzing sessions for prompt optimization...${NC}"
    echo -e "${DIM}Sessions: $sessions | Dry run: ${dry_run_param}${NC}"
    echo ""

    local response http_code
    response=$(curl -s -w "\n%{http_code}" -X POST "$url" 2>/dev/null) || true
    http_code=$(echo "$response" | tail -1)
    response=$(echo "$response" | sed '$d')
    if [ -z "$http_code" ] || [ "$http_code" = "000" ]; then
        echo -e "${RED}Error: Could not connect to dashboard API at http://${host}:${port}${NC}"
        echo "Make sure the dashboard is running: loki serve"
        exit 1
    fi
    if [ "$http_code" -ge 400 ] 2>/dev/null; then
        echo -e "${RED}Error: Dashboard API returned HTTP $http_code${NC}"
        [ -n "$response" ] && echo "$response"
        exit 1
    fi

    if ! command -v python3 &>/dev/null; then
        echo "$response" | jq . 2>/dev/null || echo "$response"
    else
        echo "$response" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read())

failures = data.get('failures_analyzed', 0)
changes = data.get('changes', [])
version = data.get('version', None)

print(f'Failures analyzed: {failures}')
print(f'Changes proposed:  {len(changes)}')
print('---')

for i, change in enumerate(changes, 1):
    prompt = change.get('prompt', '?')
    rationale = change.get('rationale', 'No rationale provided')
    old = change.get('old_value', '')
    new = change.get('new_value', '')
    print(f'')
    print(f'  {i}. {prompt}')
    print(f'     Rationale: {rationale}')
    if old:
        print(f'     Old: {old[:80]}...' if len(old) > 80 else f'     Old: {old}')
    if new:
        print(f'     New: {new[:80]}...' if len(new) > 80 else f'     New: {new}')

if version is not None:
    print(f'')
    print(f'Prompts optimized to version {version}')
elif not changes:
    print(f'')
    print('No optimization changes proposed.')
print()
" 2>/dev/null || echo "$response"
    fi
}

# ============================================================================
# Migration Command
# ============================================================================

cmd_migrate_help() {
    echo -e "${BOLD}loki migrate${NC} - Codebase migration with autonomous agents"
    echo ""
    echo "Usage: loki migrate <path-to-codebase> [options]"
    echo ""
    echo "Migrates a codebase to a new language, framework, or architecture using"
    echo "multi-agent orchestration with guardrails and incremental verification."
    echo ""
    echo "Options:"
    echo "  --target <lang|framework|arch>   Migration target (e.g. typescript, react, microservices)"
    echo "  --plan-only                      Generate migration plan without executing"
    echo "  --show-plan                      Display current migration plan"
    echo "  --phase <phase>                  Run specific phase:"
    echo "                                     understand  - Analyze codebase structure"
    echo "                                     guardrail   - Set up tests and safety nets"
    echo "                                     migrate     - Execute migration transforms"
    echo "                                     verify      - Run verification suite"
    echo "  --parallel <N>                   Max parallel agents (default: 4, max: 10)"
    echo "  --compliance <preset>            Compliance preset: healthcare, fintech, government"
    echo "  --dry-run                        Show what would change without executing"
    echo "  --resume                         Resume from last checkpoint"
    echo "  --multi-repo <glob>              Multiple repository paths (glob pattern)"
    echo "  --export-report                  Export migration report to file"
    echo "  --no-dashboard                   Disable web dashboard during migration"
    echo "  --no-docs                        Skip migration_docs/ generation"
    echo "  --list                           List all migrations"
    echo "  --status [migration-id]          Show migration status"
    echo ""
    echo "Examples:"
    echo "  loki migrate ./my-app --target typescript"
    echo "  loki migrate ./my-app --target react --plan-only"
    echo "  loki migrate ./my-app --target microservices --compliance fintech"
    echo "  loki migrate --list"
    echo "  loki migrate --status mig-20260223-001"
    echo "  loki migrate ./my-app --target typescript --resume"
    echo "  loki migrate ./my-app --target typescript --phase verify"
    echo "  loki migrate --multi-repo './services/*' --target typescript"
}

cmd_migrate_list() {
    PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
import sys
sys.path.insert(0, '.')
from dashboard.migration_engine import list_migrations
migrations = list_migrations()
if not migrations:
    print('No migrations found.')
    print('Start a migration with: loki migrate <path> --target <target>')
else:
    print(f'\033[1mMigrations\033[0m ({len(migrations)} total)')
    print('---')
    for m in migrations:
        print(f\"  {m['id']}\")
        print(f\"    Target:   {m.get('target', '?')}\")
        print(f\"    Source:   {m.get('source_path', '?')}\")
        print(f\"    Status:   {m.get('status', '?')}\")
        print(f\"    Created:  {m.get('created_at', '?')[:10] if m.get('created_at') else '?'}\")
        print()
" || {
        echo -e "${RED}Error listing migrations${NC}"
        return 1
    }
}

cmd_migrate_status() {
    local migration_id="${1:-}"
    local migrations_dir="${HOME}/.loki/migrations"

    if [ -z "$migration_id" ]; then
        # Show status of most recent migration
        local latest
        latest=$(find "$migrations_dir" -name "manifest.json" -maxdepth 2 2>/dev/null | sort | tail -1)
        if [ -z "$latest" ]; then
            echo -e "${YELLOW}No migrations found.${NC}"
            return 0
        fi
        migration_id=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1]))['id'])" "$latest" 2>/dev/null || echo "")
    fi

    local manifest="${migrations_dir}/${migration_id}/manifest.json"
    if [ ! -f "$manifest" ]; then
        echo -e "${RED}Error: Migration not found: ${migration_id}${NC}"
        echo "Run 'loki migrate --list' to see available migrations."
        return 1
    fi

    PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
import json, sys
sys.path.insert(0, '.')
from dashboard.migration_engine import MigrationPipeline
try:
    pipeline = MigrationPipeline.load(sys.argv[1])
    progress = pipeline.get_progress()
    print(json.dumps(progress, indent=2))
except Exception as e:
    print(f'\033[0;31mError reading migration: {e}\033[0m', file=sys.stderr)
    sys.exit(1)
" "$migration_id" || {
        echo -e "${RED}Error reading migration manifest${NC}"
        return 1
    }
}

cmd_migrate_show_plan() {
    local codebase_path="${1:-$(pwd)}"
    local migrations_dir="${HOME}/.loki/migrations"

    # Find plan for this codebase
    local plan_file=""
    if [ -f "${codebase_path}/.loki/migration-plan.json" ]; then
        plan_file="${codebase_path}/.loki/migration-plan.json"
    else
        # Search in global migrations dir
        local latest
        latest=$(find "$migrations_dir" -name "migration-plan.json" -maxdepth 2 2>/dev/null | sort | tail -1)
        if [ -n "$latest" ]; then
            plan_file="$latest"
        fi
    fi

    if [ -z "$plan_file" ] || [ ! -f "$plan_file" ]; then
        echo -e "${YELLOW}No migration plan found.${NC}"
        echo "Generate one with: loki migrate <path> --target <target> --plan-only"
        return 0
    fi

    echo -e "${BOLD}Migration Plan${NC}"
    echo -e "${DIM}Source: ${plan_file}${NC}"
    echo "---"

    # Try engine-based summary first, fall back to raw JSON display
    local migration_id_for_plan=""
    migration_id_for_plan=$(python3 -c "
import json, sys
with open(sys.argv[1]) as f:
    data = json.load(f)
# Try to extract migration ID from path or data
print(data.get('id', ''))
" "$plan_file" 2>/dev/null || echo "")

    if [ -n "$migration_id_for_plan" ]; then
        PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
import sys
sys.path.insert(0, '.')
from dashboard.migration_engine import MigrationPipeline
pipeline = MigrationPipeline.load(sys.argv[1])
print(pipeline.generate_plan_summary())
" "$migration_id_for_plan" 2>/dev/null || {
            # Fall back to basic display if engine fails
            python3 -c "
import json, sys
with open(sys.argv[1]) as f:
    plan = json.load(f)
print(json.dumps(plan, indent=2))
" "$plan_file" || echo -e "${RED}Error reading migration plan${NC}"
        }
    else
        python3 -c "
import json, sys
with open(sys.argv[1]) as f:
    plan = json.load(f)
print(json.dumps(plan, indent=2))
" "$plan_file" || {
            echo -e "${RED}Error reading migration plan${NC}"
            return 1
        }
    fi
}

cmd_migrate_start() {
    local codebase_path="$1"
    local target="$2"
    local plan_only="$3"
    local phase="$4"
    local parallel="$5"
    local compliance="$6"
    local dry_run="$7"
    local resume="$8"
    local multi_repo="$9"
    local export_report="${10}"
    local no_dashboard="${11:-false}"
    local no_docs="${12:-false}"

    local migrations_dir="${HOME}/.loki/migrations"
    local migration_id=""

    # Check python3 availability
    if ! command -v python3 &>/dev/null; then
        echo -e "${RED}Error: python3 is required for migration${NC}"
        return 1
    fi

    # Check for path traversal BEFORE canonicalization
    case "$codebase_path" in
        ../*|*/../*|*/..|--)
            echo -e "${RED}Error: Path traversal not allowed in codebase path${NC}"
            return 1
            ;;
    esac

    # Validate codebase path
    if [ ! -d "$codebase_path" ]; then
        echo -e "${RED}Error: Codebase path does not exist: ${codebase_path}${NC}"
        return 1
    fi

    # Then canonicalize
    codebase_path=$(cd "$codebase_path" && pwd) || {
        echo -e "${RED}Error: Cannot access $codebase_path${NC}"
        return 1
    }

    # Validate target
    if [ -z "$target" ]; then
        echo -e "${RED}Error: --target is required${NC}"
        echo "Specify the migration target (e.g. --target typescript, --target react, --target microservices)"
        echo ""
        echo "Run 'loki migrate --help' for usage."
        return 2
    fi

    # Validate compliance preset
    if [ -n "$compliance" ]; then
        case "$compliance" in
            healthcare|fintech|government) ;;
            *)
                echo -e "${RED}Error: Invalid compliance preset: ${compliance}${NC}"
                echo "Valid presets: healthcare, fintech, government"
                return 1
                ;;
        esac
    fi

    # Validate parallel count
    if [ -n "$parallel" ]; then
        if ! [[ "$parallel" =~ ^[0-9]+$ ]]; then
            echo -e "${RED}Error: --parallel must be a number${NC}"
            return 1
        fi
        if [ "$parallel" -lt 1 ] || [ "$parallel" -gt 10 ]; then
            echo -e "${RED}Error: --parallel must be between 1 and 10${NC}"
            return 1
        fi
    else
        parallel=4
    fi

    # Handle resume
    if [ "$resume" = "true" ]; then
        local latest_manifest
        latest_manifest=$(find "$migrations_dir" -name "manifest.json" -maxdepth 2 -exec python3 -c "
import json, sys
codebase = sys.argv[1]
manifests = []
for path in sys.argv[2:]:
    try:
        with open(path) as f:
            m = json.load(f)
        if m.get('source_path') == codebase and m.get('status') in ('paused', 'running', 'failed'):
            manifests.append(path)
    except: pass
if manifests:
    print(manifests[-1])
" "$codebase_path" {} + 2>/dev/null || echo "")

        if [ -z "$latest_manifest" ]; then
            echo -e "${RED}Error: No resumable migration found for ${codebase_path}${NC}"
            echo "Start a new migration with: loki migrate ${codebase_path} --target ${target}"
            return 1
        fi

        migration_id=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1]))['id'])" "$latest_manifest" 2>/dev/null)
        echo -e "${GREEN}Resuming migration: ${migration_id}${NC}"
        echo -e "${DIM}Loading checkpoint...${NC}"
    fi

    # Handle multi-repo
    if [ -n "$multi_repo" ]; then
        local repo_count=0
        local repos=()
        local _repo
        # Use an array to safely handle glob expansion with spaces in paths
        local _expanded_repos=()
        # Save and restore globbing options
        local _old_nullglob=$(shopt -p nullglob 2>/dev/null || true)
        shopt -s nullglob
        local _expanded_repos=($multi_repo)
        eval "$_old_nullglob" 2>/dev/null || true
        for _repo in "${_expanded_repos[@]}"; do
            if [ -d "$_repo" ]; then
                repos+=("$_repo")
                repo_count=$((repo_count + 1))
            fi
        done
        if [ "$repo_count" -eq 0 ]; then
            echo -e "${RED}Error: No repositories found matching: ${multi_repo}${NC}"
            return 1
        fi
        # ORCHESTRATION (v9.49.0): --multi-repo now MIGRATES each discovered
        # repository, one at a time, instead of only listing them.
        #
        # WHY A WRAPPER AND NOT SURGERY INSIDE THIS FUNCTION: cmd_migrate_start
        # is self-contained per codebase_path (manifest, secret scan, phases)
        # and was measured to have 20 `return`s and ZERO `exit`s, so it is
        # safe to call in a loop -- a failing repo returns a code instead of
        # killing the run. Recursing once per repo reuses every existing gate
        # rather than reimplementing the pipeline.
        #
        # PARTIAL-FAILURE SEMANTICS: continue-and-report, decided on the
        # codebase's own precedent, not preference:
        #   - the task schema already has `blocked` as a first-class state
        #     alongside `completed` (mcp/server.py:888), so "some units did not
        #     finish" is already expressible
        #   - run.sh carries 562 best-effort sites: the house style is degrade
        #     honestly, do not abort
        #   - fail-closed is reserved for DEPENDENCY violations (a migrate phase
        #     refusing to run without its predecessor's artifacts, :17575), and
        #     independent repositories have no such dependency on each other
        # One repo failing therefore must not deny the other four their result.
        # The per-repo status table plus a non-zero aggregate exit code is what
        # makes that honest rather than silent.
        #
        # LOKI_MULTI_REPO_SEQUENTIAL=0 is NOT offered: these share a migrations
        # dir and a dashboard port, and parallel runs would interleave output
        # into an unreadable report.
        if [ "$repo_count" -gt 1 ] && [ "${_LOKI_MR_CHILD:-0}" != "1" ]; then
            echo -e "${BOLD}Multi-repo migration${NC} ($repo_count repositories, sequential)"
            echo "---"
            for repo in "${repos[@]}"; do echo "  - $repo"; done
            echo ""

            local _mr_ok=0 _mr_bad=0
            local _mr_rows=""
            local _mr_repo _mr_rc
            for _mr_repo in "${repos[@]}"; do
                echo ""
                echo -e "${CYAN}=== [$((_mr_ok + _mr_bad + 1))/$repo_count] $_mr_repo ===${NC}"
                _mr_rc=0
                # _LOKI_MR_CHILD guards the recursion: the child sees the same
                # --multi-repo argument and must NOT re-enter this block.
                _LOKI_MR_CHILD=1 cmd_migrate_start \
                    "$_mr_repo" "$target" "$plan_only" "$phase" "$parallel" \
                    "$compliance" "$dry_run" "$resume" "" "$export_report" \
                    "$no_dashboard" "$no_docs" || _mr_rc=$?
                if [ "$_mr_rc" -eq 0 ]; then
                    _mr_ok=$((_mr_ok + 1))
                    _mr_rows="${_mr_rows}  OK       ${_mr_repo}"$'\n'
                else
                    _mr_bad=$((_mr_bad + 1))
                    _mr_rows="${_mr_rows}  BLOCKED  ${_mr_repo} (exit ${_mr_rc})"$'\n'
                fi
            done

            echo ""
            echo -e "${BOLD}Multi-repo result${NC}"
            echo "---"
            printf '%s' "$_mr_rows"
            echo ""
            echo "  ${_mr_ok} of ${repo_count} migrated, ${_mr_bad} blocked."
            if [ "$_mr_bad" -gt 0 ]; then
                # A non-zero exit is what keeps "continue" from becoming
                # "silently tolerate": CI and `loki next` both branch on it.
                echo -e "${YELLOW}Some repositories did not complete. Re-run just those, or inspect them with 'loki why'.${NC}"
                return 1
            fi
            return 0
        fi

        # HONESTY (single-repo, or a --multi-repo glob that matched exactly one):
        # this flag DISCOVERS repositories; it does not migrate them.
        # `repos[]` is populated here and never referenced again -- everything
        # below operates on the single "$codebase_path". The old message said
        # "Multi-repo migration (3 repositories)" and listed all three, then
        # created one migration scoped to the first. A user reading that
        # believed three repos were migrated when one was.
        #
        # Orchestrating a real migration across repositories is a separate piece
        # of work (dependency order, per-repo gates, per-repo receipts). Until
        # that exists, this says exactly what it does and hands the user the
        # commands that cover the rest, rather than implying coverage it has not
        # delivered.
        local _mr_others=$((repo_count - 1))
        echo -e "${BOLD}Repositories found${NC} ($repo_count)"
        echo "---"
        for repo in "${repos[@]}"; do
            if [ "$repo" = "$codebase_path" ]; then
                echo "  - $repo  (migrating now)"
            else
                echo "  - $repo"
            fi
        done
        echo ""
        if [ "$_mr_others" -gt 0 ]; then
            echo -e "${YELLOW}--multi-repo discovers repositories; it does not migrate them together.${NC}"
            echo "This run migrates only: $codebase_path"
            echo ""
            echo "To cover the others, run each one:"
            for repo in "${repos[@]}"; do
                [ "$repo" = "$codebase_path" ] && continue
                echo "  loki modernize migrate \"$repo\" --target \"$target\""
            done
            echo ""
        fi
    fi

    local migration_dir
    if [ -n "$migration_id" ]; then
        # Resume: load existing migration directory
        migration_dir="${migrations_dir}/${migration_id}"
        if [ ! -d "$migration_dir" ]; then
            echo -e "${RED}Error: Migration directory not found: ${migration_dir}${NC}"
            return 1
        fi
    else
        # Create new migration via engine
        local create_result
        create_result=$(PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
import json, sys
sys.path.insert(0, '.')
from dashboard.migration_engine import MigrationPipeline
pipeline = MigrationPipeline(sys.argv[1], sys.argv[2])
manifest = pipeline.create_manifest()
print(json.dumps({'id': manifest.id, 'dir': str(pipeline.migration_dir)}))
" "$codebase_path" "$target" 2>&1) || {
            echo -e "${RED}Error: Failed to create migration${NC}"
            echo "$create_result"
            return 1
        }
        migration_id=$(echo "$create_result" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
        migration_dir=$(echo "$create_result" | python3 -c "import json,sys; print(json.load(sys.stdin)['dir'])")
    fi

    # Security scan (first step of understand phase)
    echo -e "${BOLD}[Security] Scanning for secrets...${NC}"
    local secrets_found=0
    if command -v grep &>/dev/null; then
        local secret_patterns="(password|secret|api_key|apikey|token|private_key|AWS_SECRET|DB_PASS)\\s*[=:]\\s*['\"][^'\"]+['\"]"
        secrets_found=$(grep -rEli "$secret_patterns" "$codebase_path" --include='*.js' --include='*.ts' --include='*.py' --include='*.rb' --include='*.go' --include='*.java' --include='*.env' --include='*.yml' --include='*.yaml' --include='*.json' --include='*.toml' --include='*.cfg' --include='*.ini' 2>/dev/null || true)
        if [ -n "$secrets_found" ]; then
            secrets_found=$(echo "$secrets_found" | wc -l | tr -d ' ')
        else
            secrets_found=0
        fi
    fi

    if [ "$secrets_found" -gt 0 ]; then
        echo -e "${RED}WARNING: Found $secrets_found file(s) potentially containing secrets${NC}"
        echo -e "${YELLOW}Review these files before migration to avoid leaking credentials.${NC}"
        echo ""
    else
        echo -e "${GREEN}No secrets detected.${NC}"
        echo ""
    fi

    # Handle dry-run
    if [ "$dry_run" = "true" ]; then
        echo -e "${BOLD}[Dry Run] Migration preview${NC}"
        echo "---"
        echo -e "  Codebase:    ${codebase_path}"
        echo -e "  Target:      ${target}"
        echo -e "  Parallel:    ${parallel} agents"
        [ -n "$compliance" ] && echo -e "  Compliance:  ${compliance}"
        [ -n "$multi_repo" ] && echo -e "  Multi-repo:  ${multi_repo}"
        echo ""
        echo -e "${DIM}Phases that would execute:${NC}"
        echo "  1. understand  - Analyze codebase structure and dependencies"
        echo "  2. guardrail   - Set up test safety nets and rollback points"
        echo "  3. migrate     - Execute migration transforms"
        echo "  4. verify      - Run verification suite and regression tests"
        echo ""
        echo -e "${YELLOW}No changes were made (dry-run mode).${NC}"
        # Clean up migration dir created by engine
        rm -rf "$migration_dir" 2>/dev/null || true
        return 0
    fi

    emit_event migration cli start "id=$migration_id" "target=$target" "source=$codebase_path" 2>/dev/null || true

    echo -e "${BOLD}Migration: ${migration_id}${NC}"
    echo "---"
    echo -e "  Codebase:    ${codebase_path}"
    echo -e "  Target:      ${target}"
    echo -e "  Parallel:    ${parallel} agents"
    [ -n "$compliance" ] && echo -e "  Compliance:  ${compliance}"
    echo ""

    # Dashboard cleanup trap for error paths
    local migrate_dashboard_pid=""
    _migrate_cleanup_dashboard() {
        if [ -n "$migrate_dashboard_pid" ]; then
            kill "$migrate_dashboard_pid" 2>/dev/null || true
            wait "$migrate_dashboard_pid" 2>/dev/null || true
            rm -f "${codebase_path}/.loki/dashboard/dashboard.pid" 2>/dev/null || true
        fi
    }
    trap '_migrate_cleanup_dashboard' RETURN

    # Start dashboard for migration monitoring (unless --no-dashboard)
    if [ "$no_dashboard" != "true" ]; then
        local dashboard_port="${LOKI_DASHBOARD_PORT:-57374}"
        local skill_dir="${SKILL_DIR:-.}"
        local dashboard_venv="$HOME/.loki/dashboard-venv"
        local python_cmd="python3"
        local url_scheme="http"

        # Use venv python if available
        if [ -x "${dashboard_venv}/bin/python3" ]; then
            python_cmd="${dashboard_venv}/bin/python3"
        fi

        # Check if dashboard deps are available
        if "$python_cmd" -c "import fastapi" 2>/dev/null; then
            # Find available port (increment until a free port is found)
            local port_attempts=0
            while lsof -nP -iTCP:"$dashboard_port" -sTCP:LISTEN &>/dev/null && [ $port_attempts -lt 10 ]; do
                dashboard_port=$((dashboard_port + 1))
                port_attempts=$((port_attempts + 1))
            done

            if [ $port_attempts -lt 10 ]; then
                # Determine TLS
                if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
                    url_scheme="https"
                fi

                if ! mkdir -p "${codebase_path}/.loki/dashboard/logs" 2>/dev/null; then
                    echo -e "  ${YELLOW}Dashboard skipped (cannot create log directory)${NC}"
                else
                    local log_file="${codebase_path}/.loki/dashboard/logs/dashboard.log"

                    LOKI_DASHBOARD_PORT="$dashboard_port" \
                    LOKI_DASHBOARD_HOST="127.0.0.1" \
                    LOKI_PROJECT_PATH="$codebase_path" \
                    LOKI_SKILL_DIR="$skill_dir" \
                    LOKI_TLS_CERT="${LOKI_TLS_CERT:-}" \
                    LOKI_TLS_KEY="${LOKI_TLS_KEY:-}" \
                    PYTHONPATH="$skill_dir" \
                        nohup "$python_cmd" -m dashboard.server > "$log_file" 2>&1 &
                    migrate_dashboard_pid=$!

                    echo "$migrate_dashboard_pid" > "${codebase_path}/.loki/dashboard/dashboard.pid"

                    sleep 2
                    if kill -0 "$migrate_dashboard_pid" 2>/dev/null; then
                        echo -e "  Dashboard API: ${CYAN}${url_scheme}://127.0.0.1:${dashboard_port}/${NC} (UI: loki ui)"
                        echo ""
                    else
                        echo -e "  ${YELLOW}Dashboard failed to start (check logs)${NC}"
                        migrate_dashboard_pid=""
                        echo ""
                    fi
                fi
            fi
        fi
    fi

    # Determine which phases to run
    local phases_to_run=()
    if [ -n "$phase" ]; then
        case "$phase" in
            understand|guardrail|migrate|verify)
                phases_to_run=("$phase")
                ;;
            *)
                echo -e "${RED}Error: Invalid phase: ${phase}${NC}"
                echo "Valid phases: understand, guardrail, migrate, verify"
                return 1
                ;;
        esac
    elif [ "$plan_only" = "true" ]; then
        phases_to_run=("understand" "guardrail")
    else
        phases_to_run=("understand" "guardrail" "migrate" "verify")
    fi

    # Validate prerequisite artifacts exist before running phases (#81)
    for p in "${phases_to_run[@]}"; do
        case "$p" in
            guardrail)
                if [ ! -f "${migration_dir}/docs/analysis.md" ] || [ ! -f "${migration_dir}/seams.json" ]; then
                    echo -e "${RED}Error: Phase 'guardrail' requires artifacts from 'understand' phase${NC}"
                    echo -e "${DIM}  Missing: analysis.md or seams.json in ${migration_dir}${NC}"
                    echo "Run: loki migrate ${codebase_path} --target ${target} --phase understand"
                    return 1
                fi
                ;;
            migrate)
                if [ ! -f "${migration_dir}/features.json" ]; then
                    echo -e "${RED}Error: Phase 'migrate' requires artifacts from 'guardrail' phase${NC}"
                    echo -e "${DIM}  Missing: features.json in ${migration_dir}${NC}"
                    echo "Run: loki migrate ${codebase_path} --target ${target} --phase guardrail"
                    return 1
                fi
                ;;
            verify)
                if [ ! -f "${migration_dir}/migration-plan.json" ]; then
                    echo -e "${RED}Error: Phase 'verify' requires migration-plan.json from 'migrate' phase${NC}"
                    echo -e "${DIM}  Missing: migration-plan.json in ${migration_dir}${NC}"
                    echo "Run: loki migrate ${codebase_path} --target ${target} --phase migrate"
                    return 1
                fi
                ;;
        esac
    done

    # Execute phases
    for p in "${phases_to_run[@]}"; do
        echo -e "${CYAN}[Phase: ${p}]${NC} Starting..."

        # Start phase via engine
        PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
import sys
sys.path.insert(0, '.')
from dashboard.migration_engine import MigrationPipeline
pipeline = MigrationPipeline.load(sys.argv[1])
pipeline.start_phase(sys.argv[2])
" "$migration_id" "$p" || {
            echo -e "${RED}Error: Failed to start phase $p${NC}"
            return 1
        }

        # Phase-specific logic -- invoke Claude to do real work
        local phase_prompt=""
        case "$p" in
            understand)
                echo -e "  ${DIM}Analyzing codebase structure...${NC}"
                phase_prompt="You are performing the UNDERSTAND phase of a codebase migration.
Target: ${target}
Codebase: ${codebase_path}

Tasks:
1. Analyze the full codebase structure (languages, frameworks, dependencies, architecture)
2. Create the docs directory: mkdir -p ${migration_dir}/docs
3. Write analysis documentation to ${migration_dir}/docs/analysis.md
4. Identify migration seams (logical boundaries for incremental migration) and write them to ${migration_dir}/seams.json as a JSON array of objects with fields: id (string, e.g. 'seam-01'), name (string), description (string), type (string: 'module'/'api'/'config'/'adapter'), files (array of file paths), dependencies (array of seam ids), priority (string: 'high'/'medium'/'low')

You MUST create both files. The migration cannot proceed without them.
Write the analysis doc first, then the seams.json."
                ;;
            guardrail)
                echo -e "  ${DIM}Setting up safety nets and characterization tests...${NC}"
                phase_prompt="You are performing the GUARDRAIL phase of a codebase migration.
Target: ${target}
Codebase: ${codebase_path}
Migration dir: ${migration_dir}

Read ${migration_dir}/docs/analysis.md and ${migration_dir}/seams.json for context.

Tasks:
1. Identify existing tests and create characterization tests that capture current behavior
2. Write ${migration_dir}/features.json as a JSON array of objects with fields: id (string, e.g. 'F01'), category (string, e.g. 'core'), description (string), characterization_test (string, shell command to verify), passes (boolean, set to true for existing passing behavior), risk (string: 'low'/'medium'/'high')
3. Create a git checkpoint: cd ${codebase_path} && git stash || true

All features in features.json must have passes: true for the gate to pass."
                ;;
            migrate)
                echo -e "  ${DIM}Executing migration transforms...${NC}"
                phase_prompt="You are performing the MIGRATE phase of a codebase migration.
Target: ${target}
Codebase: ${codebase_path}
Migration dir: ${migration_dir}

Read ${migration_dir}/docs/analysis.md, ${migration_dir}/seams.json, and ${migration_dir}/features.json for context.

Tasks:
1. Create a migration plan and write it to ${migration_dir}/migration-plan.json as a JSON object with fields: version (integer, default 1), strategy (string: 'incremental' or 'big_bang'), steps (array of objects with: id (string), description (string), type (string: 'refactor'/'rewrite'/'config'/'test'), status (string, set to 'completed' after you do the step))
2. Execute the actual code migration transforms in ${codebase_path} -- convert code from the current framework/language to ${target}
3. Update each step status to 'completed' as you finish it
4. Work incrementally seam by seam from ${migration_dir}/seams.json

All steps must have status: completed for the gate to pass."
                ;;
            verify)
                echo -e "  ${DIM}Running verification suite...${NC}"
                phase_prompt="You are performing the VERIFY phase of a codebase migration.
Target: ${target}
Codebase: ${codebase_path}
Migration dir: ${migration_dir}

Tasks:
1. Run syntax validation on all migrated files
2. Run any test commands from ${migration_dir}/features.json to verify behavior is preserved
3. Check for common migration issues (missing imports, broken references, etc.)
4. Write a verification report to ${migration_dir}/docs/verification-report.md
5. Summarize what was migrated, what works, and any remaining issues"
                ;;
        esac

        # Invoke Claude to execute the phase
        local phase_exit=0
        if [ -n "$phase_prompt" ]; then
            local provider_name="${LOKI_PROVIDER:-claude}"
            # caveman ACTIVATION (free-form narration): the phase exec writes its
            # deliverables via tools; stdout is piped to python and only DISPLAYED,
            # never captured/parsed for a verdict. So compress the narration tokens
            # at the configured level when warranted (mirrors run.sh:13167 main loop).
            # An EMPTY CAVEMAN_DEFAULT_MODE is NOT inert (caveman falls back to the
            # user global default), so when activation is not warranted we must NOT
            # set the var at all (bare branch), keeping the call byte-identical to
            # pre-caveman. No-op when caveman is absent.
            local _loki_mig_cm=""
            _loki_mig_cm="$(_loki_caveman_narration_level)"
            _loki_perm_argv_for "$codebase_path"
            case "$provider_name" in
                claude)
                    { (cd "$codebase_path" && if [ -n "$_loki_mig_cm" ]; then SHELL="$_LOKI_CLAUDE_SHELL" CAVEMAN_DEFAULT_MODE="$_loki_mig_cm" claude "${_LOKI_PERM_ARGV[@]}" -p "$phase_prompt" --output-format stream-json --verbose 2>&1; else SHELL="$_LOKI_CLAUDE_SHELL" claude "${_LOKI_PERM_ARGV[@]}" -p "$phase_prompt" --output-format stream-json --verbose 2>&1; fi) | \
                        while IFS= read -r line; do
                            # Extract text from stream-json
                            if echo "$line" | python3 -c "
import sys, json
try:
    d = json.loads(sys.stdin.read())
    if d.get('type') == 'assistant':
        for item in d.get('message', {}).get('content', []):
            if item.get('type') == 'text':
                print(item.get('text', ''), end='')
except Exception: pass
" 2>/dev/null; then
                                true
                            fi
                        done; } && phase_exit=0 || phase_exit=$?
                    ;;
                codex)
                    (cd "$codebase_path" && codex exec --sandbox workspace-write "$phase_prompt" 2>&1) || phase_exit=$?
                    ;;
                cline)
                    (cd "$codebase_path" && cline -y "$phase_prompt" 2>&1) || phase_exit=$?
                    ;;
                aider)
                    local aider_model="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
                    local aider_flags="${LOKI_AIDER_FLAGS:-}"
                    # shellcheck disable=SC2086
                    (cd "$codebase_path" && aider --message "$phase_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || phase_exit=$?
                    ;;
                opencode)
                    (cd "$codebase_path" && source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$phase_prompt" 2>&1) || phase_exit=$?
                    ;;
                *)
                    echo -e "${RED}Error: Unknown provider '${provider_name}'. Supported: claude, codex, cline, aider, opencode${NC}"
                    phase_exit=1
                    ;;
            esac
        fi

        # Check provider exit code before proceeding
        if [ "$phase_exit" -ne 0 ]; then
            echo -e "${RED}Error: Provider exited with code $phase_exit during phase $p${NC}"
            # Don't advance phase -- gate check will catch missing artifacts
        fi

        # Verify phase gate artifacts exist before advancing
        local gate_ok=true
        case "$p" in
            understand)
                if [ ! -f "${migration_dir}/docs/analysis.md" ] || [ ! -f "${migration_dir}/seams.json" ]; then
                    echo -e "${RED}Error: Phase 'understand' did not produce required artifacts${NC}"
                    echo -e "${DIM}  Expected: docs/analysis.md, seams.json${NC}"
                    gate_ok=false
                fi
                ;;
            guardrail)
                if [ ! -f "${migration_dir}/features.json" ]; then
                    echo -e "${RED}Error: Phase 'guardrail' did not produce required artifacts${NC}"
                    echo -e "${DIM}  Expected: features.json${NC}"
                    gate_ok=false
                fi
                ;;
            migrate)
                if [ ! -f "${migration_dir}/migration-plan.json" ]; then
                    echo -e "${RED}Error: Phase 'migrate' did not produce required artifacts${NC}"
                    echo -e "${DIM}  Expected: migration-plan.json${NC}"
                    gate_ok=false
                fi
                ;;
        esac

        if [ "$gate_ok" != "true" ]; then
            echo -e "${YELLOW}Phase '$p' artifacts missing. Migration halted.${NC}"
            echo -e "${DIM}Re-run with: loki migrate <path> --target ${target} --phase ${p} --resume${NC}"
            return 1
        fi

        # Complete phase via engine
        PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
import sys
sys.path.insert(0, '.')
from dashboard.migration_engine import MigrationPipeline
pipeline = MigrationPipeline.load(sys.argv[1])
pipeline.advance_phase(sys.argv[2])
" "$migration_id" "$p" || {
            echo -e "${RED}Error: Failed to complete phase $p${NC}"
            return 1
        }

        echo -e "${GREEN}  [Phase: ${p}] Complete${NC}"
        echo ""
    done

    # Generate migration_docs/ in the codebase (unless --no-docs or --plan-only)
    if [ "$plan_only" != "true" ] && [ "$no_docs" != "true" ]; then
        echo -e "${CYAN}[Phase: document]${NC} Generating migration documentation..."

        local doc_prompt="You are generating comprehensive migration documentation for a completed codebase migration.

Target: ${target}
Codebase: ${codebase_path}
Migration dir: ${migration_dir}

Read the following files for context:
- ${migration_dir}/docs/analysis.md (original codebase analysis)
- ${migration_dir}/docs/verification-report.md (verification results)
- ${migration_dir}/features.json (feature tracking)
- ${migration_dir}/seams.json (migration boundaries)
- ${migration_dir}/migration-plan.json (migration steps)

Create a directory called migration_docs/ in the codebase root (${codebase_path}/migration_docs/) with the following files:

1. **README.md** - Executive summary of the migration:
   - What the codebase was before (source language/framework/architecture)
   - What it is now (target: ${target})
   - High-level summary of changes made
   - Link to other docs in migration_docs/

2. **USAGE.md** - How to use the migrated codebase:
   - Getting started / quick start
   - Key commands and workflows
   - Configuration options
   - Environment variables

3. **INSTALLATION.md** - Installation and setup:
   - Prerequisites and dependencies
   - Step-by-step installation
   - Build instructions
   - Development environment setup

4. **TESTING.md** - Testing the migrated codebase:
   - How to run tests
   - Test coverage status
   - Known test gaps
   - How to add new tests

5. **WHAT-CHANGED.md** - Detailed migration changelog:
   - Files added, modified, and removed
   - Architecture changes
   - API changes (if any)
   - Configuration changes
   - Dependency changes

6. **WHAT-WORKS.md** - Current status:
   - Features confirmed working (from verification)
   - Features that need manual testing
   - Known limitations

7. **WHAT-PENDING.md** - Remaining work:
   - Items flagged during verification
   - Manual steps required
   - Recommended follow-up tasks
   - Security items to address

8. **DEPLOYMENT.md** - Deployment guide (if applicable):
   - How to deploy the migrated application
   - CI/CD considerations
   - Infrastructure changes needed
   - Rollback instructions

IMPORTANT RULES:
- Only document what is TRUE and VERIFIED - never fabricate features or capabilities
- Reference actual files and test results from the migration artifacts
- If something was not tested or verified, say so explicitly
- Mark uncertain items with 'NEEDS VERIFICATION' tags
- Do NOT use emojis anywhere in the documentation"

        local doc_exit=0
        local provider_name="${LOKI_PROVIDER:-claude}"
        # caveman ACTIVATION (free-form narration): docs-gen writes the docs via
        # tools; stdout is piped to python and only DISPLAYED, never captured/parsed.
        # Compress narration at the configured level when warranted; empty -> bare
        # branch, byte-identical to pre-caveman (empty mode is NOT inert). No-op absent.
        local _loki_doc_cm=""
        _loki_doc_cm="$(_loki_caveman_narration_level)"
        _loki_perm_argv_for "$codebase_path"
        case "$provider_name" in
            claude)
                { (cd "$codebase_path" && if [ -n "$_loki_doc_cm" ]; then SHELL="$_LOKI_CLAUDE_SHELL" CAVEMAN_DEFAULT_MODE="$_loki_doc_cm" claude "${_LOKI_PERM_ARGV[@]}" -p "$doc_prompt" --output-format stream-json --verbose 2>&1; else SHELL="$_LOKI_CLAUDE_SHELL" claude "${_LOKI_PERM_ARGV[@]}" -p "$doc_prompt" --output-format stream-json --verbose 2>&1; fi) | \
                    while IFS= read -r line; do
                        if echo "$line" | python3 -c "
import sys, json
try:
    d = json.loads(sys.stdin.read())
    if d.get('type') == 'assistant':
        for item in d.get('message', {}).get('content', []):
            if item.get('type') == 'text':
                print(item.get('text', ''), end='')
except Exception: pass
" 2>/dev/null; then
                            true
                        fi
                    done; } && doc_exit=0 || doc_exit=$?
                ;;
            codex)
                (cd "$codebase_path" && codex exec --sandbox workspace-write "$doc_prompt" 2>&1) || doc_exit=$?
                ;;
            cline)
                (cd "$codebase_path" && cline -y "$doc_prompt" 2>&1) || doc_exit=$?
                ;;
            aider)
                local aider_model="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
                local aider_flags="${LOKI_AIDER_FLAGS:-}"
                # shellcheck disable=SC2086
                (cd "$codebase_path" && aider --message "$doc_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || doc_exit=$?
                ;;
        esac

        if [ "$doc_exit" -eq 0 ] && [ -d "${codebase_path}/migration_docs" ]; then
            local doc_count
            doc_count=$(find "${codebase_path}/migration_docs" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
            echo -e "${GREEN}  [Phase: document] Complete (${doc_count} docs generated)${NC}"
        else
            echo -e "${YELLOW}  [Phase: document] Warning: Documentation may be incomplete${NC}"
        fi
        echo ""
    fi

    # Plan-only: generate plan file and stop
    if [ "$plan_only" = "true" ]; then
        echo -e "${BOLD}Migration plan generated.${NC}"
        echo -e "View with: loki migrate --show-plan"

        python3 -c "
import json, sys
from datetime import datetime
plan = {
    'target': sys.argv[1],
    'source_path': sys.argv[2],
    'estimated_hours': '?',
    'risk_level': 'medium',
    'steps': [],
    'dependencies': [],
    'breaking_changes': [],
    'generated_at': datetime.now().isoformat()
}
with open(sys.argv[3], 'w') as f:
    json.dump(plan, f, indent=2)
" "$target" "$codebase_path" "${migration_dir}/migration-plan.json" || {
            echo -e "${RED}Error: Failed to create migration plan${NC}"
            return 1
        }

        # Update status to paused
        python3 -c "
import json, sys
from datetime import datetime
manifest_path = sys.argv[1]
with open(manifest_path) as f:
    m = json.load(f)
m['status'] = 'paused'
m['updated_at'] = datetime.now().isoformat()
with open(manifest_path, 'w') as f:
    json.dump(m, f, indent=2)
" "${migration_dir}/manifest.json" || {
            echo -e "${RED}Error: Failed to update migration state${NC}"
            return 1
        }
        return 0
    fi

    # Mark migration complete
    python3 -c "
import json, sys
from datetime import datetime
manifest_path = sys.argv[1]
with open(manifest_path) as f:
    m = json.load(f)
m['status'] = 'completed'
m['progress_pct'] = 100
m['updated_at'] = datetime.now().isoformat()
with open(manifest_path, 'w') as f:
    json.dump(m, f, indent=2)
" "${migration_dir}/manifest.json" || {
        echo -e "${RED}Error: Failed to update migration state${NC}"
        return 1
    }

    emit_event migration cli complete "id=$migration_id" "target=$target" 2>/dev/null || true

    # Export report if requested
    if [ "$export_report" = "true" ]; then
        local report_file="${codebase_path}/migration-report-${migration_id}.json"
        cp "${migration_dir}/manifest.json" "$report_file" 2>/dev/null
        echo -e "${GREEN}Report exported: ${report_file}${NC}"
    fi

    echo -e "${GREEN}${BOLD}Migration complete: ${migration_id}${NC}"
    echo -e "View details: loki migrate --status ${migration_id}"
}

#===============================================================================
# loki heal - Legacy System Healing (v6.67.0)
# Inspired by Amazon AGI Lab's "How Agentic AI Helps Heal Systems We Can't Replace"
# Modernize legacy codebases incrementally without breaking existing behavior.
#===============================================================================

# ---------------------------------------------------------------------------
# loki verify - Autonomi Verify (Verification-as-a-Service MVP)
#
# Thin dispatcher that sources autonomy/verify.sh and delegates to its
# verify_main(). The verification core is deliberately standalone (it does NOT
# enter the autonomous iteration loop): it computes a PR-style merge-base diff
# and runs deterministic gates against the current tree, emitting a verdict and
# a consolidated evidence document. Deterministic-only in this MVP (no LLM
# review). Exit code is propagated to the caller so the command is CI-gate
# usable.
# ---------------------------------------------------------------------------
cmd_verify() {
    # One route decision for every engine10-only verify form. --pubkey FILE / --pubkey=FILE
    # (any position) verifies a receipt against a given public key (no ~/.loki needed). Never
    # exec bin/loki from here: under LOKI_LEGACY_BASH=1 bin/loki execs this script straight
    # back, an unbounded exec loop. A workspace composite (composite.json, or --seal-composite
    # RUN_DIR) is verified by the engine10 verifier directly; bin/loki sends a bare verify here.
    local _pk_arg _pk_rc
    for _pk_arg in "$@"; do
        case "$_pk_arg" in
            --pubkey|--pubkey=*|--require-exogenous|--sign-deploy|--sign-provenance|--url|--receipt|composite.json|*/composite.json|--seal-composite|--seal-composite=*|--max-age|--max-age=*|.loki/groups/*/receipt.json|*/.loki/groups/*/receipt.json|.loki/runs/e10-*/receipt.json|*/.loki/runs/e10-*/receipt.json)
                # The delegate execs on success, so reaching the next line means it refused.
                _pk_rc=0
                _loki_bun_delegate engine10 verify "$@" || _pk_rc=$?
                echo "loki verify: $_pk_arg needs the Bun engine, which is unavailable: ${_LOKI_DELEGATE_WHY:-bun route unavailable}" >&2
                [ -z "${_LOKI_DELEGATE_FIX:-}" ] || echo "Fix: ${_LOKI_DELEGATE_FIX}" >&2
                return "$_pk_rc" ;;
            # A signed deploy record (.loki/deploys/*.json, schema loki.deploy/1) or a run receipt anywhere (schema loki.v10.receipt) is re-checked by the same verifier.
            *.json)
                if [ -f "$_pk_arg" ] && grep -Eq 'loki\.deploy/1|"schema" *: *"loki\.v10\.receipt' "$_pk_arg" 2>/dev/null; then
                    _loki_bun_delegate engine10 verify "$@" || return $?
                    return 0
                fi ;;
        esac
    done
    # --fast: deterministic checks only, in milliseconds. Measured on this repo
    # (1,932 tracked files): 298 ms cold, 87 ms warm, 19 ms diff-scoped, against
    # an 11,040 ms shell-based baseline.
    #
    # It runs ONLY exogenous checks -- no model call, no network -- so every
    # verdict is reproducible by anyone with the same commit. That is the point,
    # not a limitation: a verdict you can re-derive is a fact, and a fact is what
    # an IDE, a CI step, or another vendor's agent can safely act on without
    # trusting our model choices.
    #
    # The full `loki verify` remains the deeper, slower path.
    for _fv_arg in "$@"; do
        if [ "$_fv_arg" = "--fast" ]; then
            local _fv_py="$_LOKI_SCRIPT_DIR/lib/fast_verify.py"
            if [ ! -f "$_fv_py" ]; then
                echo -e "${RED}Error: fast_verify not found at $_fv_py${NC}" >&2
                return 3
            fi
            local _fv_argv=()
            for _a in "$@"; do
                [ "$_a" = "--fast" ] || _fv_argv+=("$_a")
            done
            python3 "$_fv_py" "${_fv_argv[@]+"${_fv_argv[@]}"}"
            return $?
        fi
    done

    local verify_mod="$_LOKI_SCRIPT_DIR/verify.sh"
    if [ ! -f "$verify_mod" ]; then
        echo -e "${RED}Error: verify module not found at $verify_mod${NC}" >&2
        return 3
    fi
    # shellcheck source=/dev/null
    source "$verify_mod"
    # verify_main handles --json natively (evidence document on stdout, banner
    # on stderr); the generic json-output.sh wrapper must not intercept it.
    verify_main "$@"
    return $?
}

# ---------------------------------------------------------------------------
# loki spec - the living spec (drift detection + lock)
#
# Thin dispatcher that sources autonomy/spec.sh and delegates to spec_main().
# The spec core is deliberately standalone (it does NOT enter the autonomous
# loop): it binds spec requirements to content hashes (.loki/spec/spec.lock)
# and detects drift deterministically, emitting .loki/spec/drift-report.json.
# Exit codes are propagated so `loki spec status` is CI-gate usable.
# ---------------------------------------------------------------------------
# loki intent: does the spec still say what was actually wanted?
#
# Our gates prove code matches spec. They cannot prove the spec was RIGHT --
# 8090 AI documents a build where "the software converged with the
# interpretation. The interpretation had diverged from the intent", which a
# perfect verification gate passes. This measures that gap deterministically by
# comparing the requirement hash an intent was affirmed against with the
# requirement's hash now. No model judges anything; see autonomy/intent.sh.
cmd_intent() {
    local intent_mod="$_LOKI_SCRIPT_DIR/intent.sh"
    if [ ! -f "$intent_mod" ]; then
        echo -e "${RED}Error: intent module not found at $intent_mod${NC}" >&2
        return 3
    fi
    # shellcheck source=/dev/null
    source "$intent_mod"
    intent_main "$@"
}

cmd_spec() {
    local spec_mod="$_LOKI_SCRIPT_DIR/spec.sh"
    if [ ! -f "$spec_mod" ]; then
        echo -e "${RED}Error: spec module not found at $spec_mod${NC}" >&2
        return 3
    fi
    # shellcheck source=/dev/null
    source "$spec_mod"
    spec_main "$@"
    return $?
}

# ---------------------------------------------------------------------------
# loki grill - spec interrogation (Devil's-Advocate, pre-build)
#
# Thin dispatcher that sources autonomy/grill.sh and delegates to grill_main().
# Invokes the provider once to produce the hardest questions exposing spec
# weaknesses; writes .loki/grill/report.md. Requires the provider CLI and
# fails cleanly when it is absent (no fabricated questions).
# ---------------------------------------------------------------------------
cmd_grill() {
    local grill_mod="$_LOKI_SCRIPT_DIR/grill.sh"
    if [ ! -f "$grill_mod" ]; then
        echo -e "${RED}Error: grill module not found at $grill_mod${NC}" >&2
        return 3
    fi
    # shellcheck source=/dev/null
    source "$grill_mod"
    grill_main "$@"
    return $?
}

# ---------------------------------------------------------------------------
# loki mcp - launch the MCP (Model Context Protocol) server
#
# Thin dispatcher that sources autonomy/mcp-launch.sh and delegates to
# mcp_launch_main(). The launcher checks for python3 + the MCP SDK and, when
# the SDK is missing, offers a consent-gated bootstrap into a project-local
# virtualenv (.loki/mcp-venv) before exec'ing the server over stdio. This
# closes the fresh-npm-consumer gap where the only bin was `loki` and the
# Python MCP dependencies were never installed (task 562).
# ---------------------------------------------------------------------------
cmd_mcp() {
    local mcp_mod="$_LOKI_SCRIPT_DIR/mcp-launch.sh"
    if [ ! -f "$mcp_mod" ]; then
        echo -e "${RED}Error: mcp launcher not found at $mcp_mod${NC}" >&2
        return 3
    fi
    # shellcheck source=/dev/null
    source "$mcp_mod"
    mcp_launch_main "$@"
    return $?
}

cmd_heal_help() {
    echo -e "${BOLD}loki heal${NC} - Legacy system healing"
    echo ""
    echo "Heal legacy codebases by understanding their real behaviors -- the quirks,"
    echo "delays, error states, and invisible dependencies -- then modernizing"
    echo "incrementally while preserving institutional logic."
    echo ""
    echo "Usage: loki heal <path-to-codebase> [options]"
    echo ""
    echo "Phases:"
    echo "  archaeology    Extract knowledge, map dependencies, catalog friction"
    echo "  stabilize      Add observability and tests without changing behavior"
    echo "  isolate        Create adapter boundaries between components"
    echo "  modernize      Replace components one at a time behind adapters"
    echo "  validate       Verify behavioral equivalence with baseline"
    echo ""
    echo "Options:"
    echo "  --assess             Read-only modernization readiness triage (maturity"
    echo "                       level + ranked where-to-start targets); no changes"
    echo "  --json               With --assess: emit the readiness report as JSON"
    echo "  --phase PHASE        Start from specific phase (default: archaeology)"
    echo "  --resume             Resume healing from last checkpoint"
    echo "  --status             Show healing progress"
    echo "  --report             Generate healing report"
    echo "  --strict             Block ALL behavioral changes without approval"
    echo "  --archaeology-only   Extract knowledge only, don't modify code"
    echo "  --friction-map       Show friction map for target codebase"
    echo "  --compliance PRESET  Compliance mode (healthcare|fintech|government)"
    echo "  --provider NAME      AI provider (default: claude)"
    echo "  --parallel N         Parallel healing agents (default: 1)"
    echo "  --no-dashboard       Disable web dashboard"
    echo "  --dry-run            Show healing plan without executing"
    echo ""
    echo "Environment Variables:"
    echo "  LOKI_HEAL_MODE              Enable healing mode (auto-set by 'loki heal')"
    echo "  LOKI_HEAL_PHASE             Override starting phase"
    echo "  LOKI_HEAL_PRESERVE_FRICTION Warn before removing friction points (default: true)"
    echo "  LOKI_HEAL_STRICT            Block all behavioral changes (default: false)"
    echo ""
    echo "Examples:"
    echo "  loki heal ./legacy-app --assess               # Readiness triage (read-only)"
    echo "  loki heal ./legacy-app --assess --json         # Same, structured for dashboard"
    echo "  loki heal ./legacy-app                        # Full healing pipeline"
    echo "  loki heal ./legacy-app --phase archaeology     # Knowledge extraction only"
    echo "  loki heal ./legacy-app --archaeology-only      # Extract without modifying"
    echo "  loki heal ./legacy-app --resume                # Resume from checkpoint"
    echo "  loki heal ./legacy-app --strict                # Strict behavioral preservation"
    echo "  loki heal --status                             # Show healing progress"
    echo "  loki heal --friction-map ./legacy-app          # View friction map"
}

# CLI consolidation (Phase B, design item 6): the 'modernize' noun groups the
# legacy-modernization surface (heal/migrate) under one entry. It is a THIN
# dispatcher: each arm forwards verbatim to the existing cmd_* handler (no
# handler logic moves; heal and migrate keep their separate phase vocabularies
# and state). The two top-level tokens remain deprecated forwarding aliases.
cmd_modernize() {
    local subcommand="${1:-help}"
    case "$subcommand" in
        heal)     shift; cmd_heal "$@";    return $? ;;
        migrate)  shift; cmd_migrate "$@"; return $? ;;
        --help|-h|help)
            echo -e "${BOLD}loki modernize${NC} - Legacy system modernization"
            echo ""
            echo "Usage: loki modernize <subcommand> [args]"
            echo ""
            echo "Subcommands (grouped modernization surface):"
            echo "  heal <path>      Legacy system healing in phases (was: loki heal)"
            echo "  migrate <path>   Codebase migration in phases (was: loki migrate)"
            echo "  unit --repo <owner/name> --ref <ref> --unit <id>   Run one unit (remote unit job worker)"
            echo "  <repo> --to <lang>   Full modernization: oracle capture, waves, verify, ship"
            echo ""
            echo "Run 'loki modernize <subcommand> --help' for each. The old"
            echo "top-level names still work; see 'loki help aliases'."
            return 0
            ;;
        *)
            echo -e "${RED}Unknown modernize command: $subcommand${NC}" >&2
            echo "Run 'loki modernize --help' for usage." >&2
            return 1
            ;;
    esac
}

cmd_heal() {
    local codebase_path=""
    local phase="${LOKI_HEAL_PHASE:-archaeology}"
    local do_resume="false"
    local do_status="false"
    local do_report="false"
    local do_friction_map="false"
    local do_assess="false"
    local assess_json="false"
    local archaeology_only="false"
    local strict="${LOKI_HEAL_STRICT:-false}"
    local compliance=""
    local provider="${LOKI_PROVIDER:-claude}"
    local parallel="1"
    local no_dashboard="false"
    local dry_run="false"

    if [ $# -eq 0 ]; then
        cmd_heal_help
        return 0
    fi

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                cmd_heal_help
                return 0
                ;;
            --status)
                do_status="true"
                shift
                ;;
            --report)
                do_report="true"
                shift
                ;;
            --friction-map)
                do_friction_map="true"
                shift
                ;;
            --assess)
                do_assess="true"
                shift
                ;;
            --json)
                assess_json="true"
                shift
                ;;
            --phase)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --phase requires a value (archaeology|stabilize|isolate|modernize|validate)${NC}"
                    return 1
                fi
                phase="$2"
                shift 2
                ;;
            --phase=*)
                phase="${1#*=}"
                shift
                ;;
            --resume)
                do_resume="true"
                shift
                ;;
            --strict)
                strict="true"
                shift
                ;;
            --archaeology-only)
                archaeology_only="true"
                phase="archaeology"
                shift
                ;;
            --compliance)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --compliance requires a value${NC}"
                    return 1
                fi
                compliance="$2"
                shift 2
                ;;
            --provider)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --provider requires a value${NC}"
                    return 1
                fi
                provider="$2"
                shift 2
                ;;
            --parallel)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --parallel requires a value${NC}"
                    return 1
                fi
                parallel="$2"
                shift 2
                ;;
            --no-dashboard)
                no_dashboard="true"
                shift
                ;;
            --dry-run)
                dry_run="true"
                shift
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki heal --help' for usage."
                return 1
                ;;
            *)
                if [ -z "$codebase_path" ]; then
                    codebase_path="$1"
                else
                    echo -e "${RED}Error: Unexpected argument: $1${NC}"
                    return 1
                fi
                shift
                ;;
        esac
    done

    # Route to subcommands
    if [ "$do_status" = "true" ]; then
        local heal_dir="${codebase_path:-.}/.loki/healing"
        if [[ ! -d "$heal_dir" ]]; then
            echo -e "${YELLOW}No healing session found.${NC}"
            echo "Start one with: loki heal <path-to-codebase>"
            return 0
        fi
        local progress_file="$heal_dir/healing-progress.json"
        if [[ -f "$progress_file" ]]; then
            echo -e "${BOLD}Healing Progress${NC}"
            echo ""
            python3 -c "
import json, sys
with open(sys.argv[1]) as f:
    data = json.load(f)
print(f\"  Codebase:       {data.get('codebase', '?')}\")
print(f\"  Started:        {data.get('started', '?')}\")
print(f\"  Overall Health:  {data.get('overall_health', 0):.0%}\")
print()
for c in data.get('components', []):
    pct = c.get('characterization_passing', 0) / max(c.get('characterization_tests', 1), 1)
    print(f\"  [{c.get('phase', '?'):12s}] {c.get('name', '?')}\")
    print(f\"               Friction: {c.get('friction_resolved', 0)}/{c.get('friction_points', 0)} resolved\")
    print(f\"               Tests:    {c.get('characterization_passing', 0)}/{c.get('characterization_tests', 0)} passing\")
    print(f\"               Health:   {c.get('health_score', 0):.0%}\")
    print()
" "$progress_file" 2>/dev/null || echo "  Error reading progress file."
        else
            echo "  No progress data yet. Healing may still be in archaeology phase."
        fi
        return 0
    fi

    if [ "$do_friction_map" = "true" ]; then
        local friction_file="${codebase_path:-.}/.loki/healing/friction-map.json"
        if [[ ! -f "$friction_file" ]]; then
            echo -e "${YELLOW}No friction map found.${NC}"
            echo "Run archaeology first: loki heal ${codebase_path:-.} --phase archaeology"
            return 0
        fi
        echo -e "${BOLD}Friction Map${NC}"
        echo ""
        python3 -c "
import json, sys
with open(sys.argv[1]) as f:
    data = json.load(f)
for f in data.get('frictions', []):
    safe = 'YES' if f.get('safe_to_remove') else 'NO'
    cls = f.get('classification', 'unknown')
    print(f\"  [{f.get('id', '?')}] {f.get('location', '?')}\")
    print(f\"    Behavior:       {f.get('behavior', '?')}\")
    print(f\"    Classification: {cls}\")
    print(f\"    Safe to remove: {safe}\")
    if f.get('evidence'):
        print(f\"    Evidence:       {f.get('evidence')}\")
    print()
" "$friction_file" 2>/dev/null || echo "  Error reading friction map."
        return 0
    fi

    # Modernization readiness triage (rank 13). READ-ONLY: scans the target with
    # cheap deterministic heuristics (find/wc/grep counts via python's os.walk and
    # in-python line counts) and prints a readiness report to STDOUT. It writes
    # NOTHING into the target (no .loki dir, no state files) so it cannot mutate
    # the codebase and needs zero flags beyond a path. Emitted before the
    # path-init/mkdir block below, so no healing session is created.
    if [ "$do_assess" = "true" ]; then
        local assess_path="${codebase_path:-.}"
        if [[ ! -d "$assess_path" ]]; then
            echo -e "${RED}Error: Directory not found: $assess_path${NC}"
            return 1
        fi
        if ! command -v python3 >/dev/null 2>&1; then
            echo -e "${RED}Error: python3 is required for readiness assessment.${NC}"
            return 1
        fi
        LOKI_ASSESS_PATH="$assess_path" \
        LOKI_ASSESS_JSON="$assess_json" \
        python3 - <<'PYASSESS'
import json, os, re, sys

root = os.environ.get('LOKI_ASSESS_PATH', '.')
as_json = os.environ.get('LOKI_ASSESS_JSON', 'false') == 'true'

# Directories we never descend into (VCS/deps/build noise). Keeps counts honest:
# a vendored node_modules would otherwise dominate LOC and target ranking.
SKIP_DIRS = {
    '.git', '.hg', '.svn', 'node_modules', '.loki', 'vendor', 'dist', 'build',
    '.next', '.venv', 'venv', '__pycache__', '.mypy_cache', '.pytest_cache',
    'target', '.idea', '.vscode', 'coverage', '.gradle', 'bin', 'obj',
}

# Extension -> language label. Only source-ish extensions are counted for LOC;
# unknown extensions are ignored (reported as 'other' only when they carry text).
LANG_BY_EXT = {
    '.py': 'Python', '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript',
    '.cjs': 'JavaScript', '.ts': 'TypeScript', '.tsx': 'TypeScript', '.go': 'Go',
    '.rb': 'Ruby', '.java': 'Java', '.kt': 'Kotlin', '.c': 'C', '.h': 'C',
    '.cpp': 'C++', '.cc': 'C++', '.hpp': 'C++', '.cs': 'C#', '.php': 'PHP',
    '.rs': 'Rust', '.swift': 'Swift', '.scala': 'Scala', '.sh': 'Shell',
    '.bash': 'Shell', '.pl': 'Perl', '.pm': 'Perl', '.r': 'R', '.m': 'Objective-C',
    '.lua': 'Lua', '.ex': 'Elixir', '.exs': 'Elixir', '.clj': 'Clojure',
    '.vue': 'Vue', '.dart': 'Dart',
}

TEST_HINT = re.compile(r'(^|[/_.-])(test|tests|spec|specs|__tests__)([/_.-]|s?$|\.)', re.I)
TODO_RE = re.compile(r'\b(TODO|FIXME|HACK|XXX)s?\b')

# import/require line -> the referenced module basename(s). Deliberately cheap:
# we only need a proxy for how many OTHER files reference a given file, to rank
# isolated (low blast-radius) modules ABOVE coupled ones (playbook weeks 1-2).
IMPORT_PATTERNS = [
    re.compile(r'^\s*import\s+(?:[\w.]+\s+)?["\']?([\w./-]+)'),          # py/js/ts/go
    re.compile(r'^\s*from\s+([\w./-]+)\s+import'),                        # python
    re.compile(r'require\(\s*["\']([\w./-]+)["\']\s*\)'),                # node
    re.compile(r'^\s*#include\s+[<"]([\w./-]+)[>"]'),                    # c/c++
    re.compile(r'^\s*use\s+([\w:./-]+)'),                                # rust
]

files = []          # per-file records
lang_loc = {}       # language -> total LOC
lang_files = {}     # language -> file count
total_loc = 0
total_files = 0
todo_count = 0
has_tests = False
big_files = []      # (loc, relpath)
max_loc = 0         # largest file by LOC, across every file
# module reference tallies: how many files import a given basename (no-ext)
ref_counts = {}
file_records = {}   # relpath -> record

def stem(name):
    b = os.path.basename(name)
    for e in sorted(LANG_BY_EXT, key=len, reverse=True):
        if b.endswith(e):
            return b[:-len(e)]
    return os.path.splitext(b)[0]

for dirpath, dirnames, filenames in os.walk(root):
    dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith('.git')]
    for fn in filenames:
        ext = os.path.splitext(fn)[1].lower()
        lang = LANG_BY_EXT.get(ext)
        if lang is None:
            continue
        full = os.path.join(dirpath, fn)
        try:
            with open(full, 'r', encoding='utf-8', errors='replace') as fh:
                lines = fh.readlines()
        except (OSError, IOError):
            continue
        loc = len(lines)
        rel = os.path.relpath(full, root)
        total_loc += loc
        total_files += 1
        lang_loc[lang] = lang_loc.get(lang, 0) + loc
        lang_files[lang] = lang_files.get(lang, 0) + 1
        is_test = bool(TEST_HINT.search(rel))
        if is_test:
            has_tests = True
        ftodo = 0
        refs = set()
        for ln in lines:
            if TODO_RE.search(ln):
                ftodo += 1
            for pat in IMPORT_PATTERNS:
                m = pat.search(ln)
                if m:
                    refs.add(stem(m.group(1)))
        todo_count += ftodo
        rec = {
            'path': rel, 'loc': loc, 'language': lang,
            'todos': ftodo, 'is_test': is_test,
        }
        files.append(rec)
        file_records[rel] = rec
        if loc > max_loc:
            max_loc = loc
        if loc >= 500:
            big_files.append((loc, rel))
        # tally inbound references toward the imported basenames
        for r in refs:
            ref_counts[r] = ref_counts.get(r, 0) + 1

# Attach inbound-reference (blast-radius proxy) to each non-test source file.
for rec in files:
    rec['inbound_refs'] = ref_counts.get(stem(rec['path']), 0)

# --- Technical-debt / maintenance-risk signals (all deterministic) ---
big_files.sort(reverse=True)
largest_loc = max_loc
# dependency-manifest presence (a real, cheap signal; NOT a staleness figure).
manifest_names = {
    'package.json', 'requirements.txt', 'Pipfile', 'pyproject.toml', 'go.mod',
    'Cargo.toml', 'pom.xml', 'build.gradle', 'Gemfile', 'composer.json',
}
lockfile_names = {
    'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'poetry.lock',
    'Pipfile.lock', 'go.sum', 'Cargo.lock', 'Gemfile.lock', 'composer.lock',
}
found_manifests, found_lockfiles = [], []
for fn in sorted(os.listdir(root)) if os.path.isdir(root) else []:
    if fn in manifest_names:
        found_manifests.append(fn)
    if fn in lockfile_names:
        found_lockfiles.append(fn)
# dependency staleness is NOT computed offline -> reported as 'unknown', never guessed.
dep_lock_present = len(found_lockfiles) > 0 if found_manifests else None

# RUNTIME + TOP DEPENDENCIES, read from the manifest itself. Offline-safe: this
# reports what the file DECLARES, never what is current upstream (that stays
# 'unknown' -- see dependency_staleness above).
#
# Why it matters: after 'how far behind am I', the next question a legacy
# modernization buyer asks is 'what is this even built on'. Answering
# 'package.json exists' and nothing more leaves them to open the file
# themselves, which is the work they were trying to delegate.
runtime_info = {'declared_runtime': None, 'declared_dependencies': None, 'framework_hints': []}
try:
    if 'package.json' in found_manifests:
        with open(os.path.join(root, 'package.json')) as _f:
            _pkg = json.load(_f)
        _eng = (_pkg.get('engines') or {}).get('node')
        runtime_info['declared_runtime'] = ('node ' + _eng) if _eng else 'node (engine unspecified)'
        _deps = dict(_pkg.get('dependencies') or {})
        runtime_info['declared_dependencies'] = len(_deps)
        # Name only frameworks actually present. A hint is a FACT from the file,
        # not an inference about what the project "probably" is.
        for _fw in ('express', 'react', 'next', 'vue', 'angular', 'fastify', 'koa', 'nest'):
            if _fw in _deps:
                runtime_info['framework_hints'].append(_fw + ' ' + str(_deps[_fw]))
    elif 'pyproject.toml' in found_manifests or 'requirements.txt' in found_manifests:
        runtime_info['declared_runtime'] = 'python (version unspecified in manifest)'
except (OSError, ValueError):
    # A malformed manifest is not a crash and not a guess: leave the fields None
    # so the report says nothing rather than something wrong.
    pass

todo_density = round((todo_count / total_loc) * 1000, 2) if total_loc else 0.0

# --- 4-level maturity model (Ad hoc -> Planned -> Systematic -> Optimized) ---
# Deterministic rubric from cheap signals. Higher = more modern/ready.
# Ad hoc: no tests. Planned: tests exist but sparse / large files / high TODO.
# Systematic: tests + locked deps + no oversized files + low TODO density.
# Optimized: Systematic AND a strong test footprint (>=20% of source files test).
test_files = sum(1 for f in files if f['is_test'])
test_ratio = round(test_files / total_files, 3) if total_files else 0.0
if not has_tests:
    maturity = 'Ad hoc'
    maturity_level = 1
    maturity_reason = 'No test/spec files detected: changes are unguarded.'
elif (test_ratio < 0.10) or (largest_loc >= 1000) or (todo_density >= 5.0):
    maturity = 'Planned'
    maturity_level = 2
    maturity_reason = ('Tests exist but coverage footprint is thin or debt signals are high '
                       '(sparse tests / oversized files / dense TODOs).')
elif dep_lock_present and test_ratio >= 0.20:
    maturity = 'Optimized'
    maturity_level = 4
    maturity_reason = ('Strong test footprint, pinned dependencies, and no oversized files: '
                       'ready for continuous background modernization.')
else:
    maturity = 'Systematic'
    maturity_level = 3
    maturity_reason = ('Tests present with a healthy footprint and contained file sizes; '
                       'dependency pinning or test depth can still improve.')

# --- Ranked candidate targets (playbook weeks 1-2: low blast-radius FIRST) ---
# AC#2 requires isolated / low-blast-radius modules to rank ABOVE coupled ones,
# so isolation is the PRIMARY key -- it must never be traded away for size. We
# bucket by inbound-reference tier (0 refs = isolated, 1-2 = low, 3-5 = moderate,
# 6+ = highly coupled); within a tier, higher "visibility" (LOC + TODO weight)
# breaks the tie so a bigger isolated file surfaces first. A large-but-coupled
# file can therefore never outrank a small isolated one. Tests are excluded.
candidates = [f for f in files if not f['is_test']]
def blast_tier(refs):
    if refs == 0:
        return 0  # isolated
    if refs <= 2:
        return 1  # low blast radius
    if refs <= 5:
        return 2  # moderate
    return 3      # highly coupled
def visibility(f):
    # tie-break WITHIN a blast tier only; bigger / more-flagged = more worth it.
    return f['loc'] + f['todos'] * 50
for f in candidates:
    f['_tier'] = blast_tier(f['inbound_refs'])
    f['_score'] = visibility(f)
# primary: lower tier first (isolation dominates); secondary: higher visibility.
candidates.sort(key=lambda f: (f['_tier'], -f['_score'], f['path']))
top = candidates[:10]

def target_rationale(f):
    bits = []
    if f['inbound_refs'] == 0:
        bits.append('isolated (no inbound imports -> low blast radius)')
    else:
        bits.append(f"{f['inbound_refs']} inbound reference(s)")
    bits.append(f"{f['loc']} LOC")
    if f['todos']:
        bits.append(f"{f['todos']} TODO/FIXME")
    return ', '.join(bits)

lang_mix = sorted(
    ({'language': k, 'loc': v, 'files': lang_files.get(k, 0),
      'pct': round((v / total_loc) * 100, 1) if total_loc else 0.0}
     for k, v in lang_loc.items()),
    key=lambda x: -x['loc'],
)

report = {
    'codebase': os.path.abspath(root),
    'scan': {
        'total_files': total_files,
        'total_loc': total_loc,
        'language_mix': lang_mix,
    },
    'debt_signals': {
        'has_tests': has_tests,
        'test_files': test_files,
        'test_ratio': test_ratio,
        'todo_fixme_count': todo_count,
        'todo_density_per_kloc': todo_density,
        'large_files_ge_500_loc': len(big_files),
        'largest_file_loc': largest_loc,
    },
    'maintenance_risk': {
        'dependency_manifests': found_manifests,
        'lockfiles': found_lockfiles,
        'dependency_lock_present': dep_lock_present,
        'dependency_staleness': 'unknown',  # not computable offline; never guessed
        'runtime': runtime_info,
    },
    'maturity': {
        'level': maturity_level,
        'label': maturity,
        'scale': ['Ad hoc', 'Planned', 'Systematic', 'Optimized'],
        'rationale': maturity_reason,
    },
    'ranked_targets': [
        {'path': f['path'], 'loc': f['loc'], 'language': f['language'],
         'inbound_refs': f['inbound_refs'], 'todos': f['todos'],
         'blast_tier': f['_tier'],  # 0 isolated, 1 low, 2 moderate, 3 highly coupled (primary sort key)
         'visibility_score': f['_score'], 'rationale': target_rationale(f)}
        for f in top
    ],
}

if as_json:
    print(json.dumps(report, indent=2))
    sys.exit(0)

BOLD = '\033[1m'; NC = '\033[0m'; CYAN = '\033[36m'
print(f"{BOLD}Modernization Readiness Assessment{NC}")
print()
print(f"  Codebase:  {report['codebase']}")
print(f"  Files:     {total_files} source file(s)")
print(f"  LOC:       {total_loc}")
if total_files == 0:
    print()
    print("  No recognized source files found; nothing to assess.")
    sys.exit(0)
print()
print(f"{BOLD}Language mix{NC}")
for lm in lang_mix:
    print(f"  {lm['language']:<12} {lm['loc']:>8} LOC  ({lm['pct']}%, {lm['files']} files)")
print()
print(f"{BOLD}Technical-debt signals{NC}")
d = report['debt_signals']
print(f"  Tests present:       {'yes' if d['has_tests'] else 'NO'} "
      f"({d['test_files']} test file(s), {d['test_ratio']:.0%} of files)")
print(f"  TODO/FIXME/HACK:     {d['todo_fixme_count']} "
      f"({d['todo_density_per_kloc']} per KLOC)")
print(f"  Large files (>=500): {d['large_files_ge_500_loc']} "
      f"(largest {d['largest_file_loc']} LOC)")
mr = report['maintenance_risk']
lock_str = ('yes' if mr['dependency_lock_present'] else 'NO') if mr['dependency_lock_present'] is not None else 'no manifest'
print(f"  Dependency lockfile: {lock_str}")
print(f"  Dependency staleness: {mr['dependency_staleness']} (not computed offline)")
print()
print(f"{BOLD}Maturity{NC} (Ad hoc -> Planned -> Systematic -> Optimized)")
print(f"  Level {maturity_level}/4: {CYAN}{maturity}{NC}")
print(f"  {maturity_reason}")
print()
print(f"{BOLD}Where to start{NC} (ranked low-blast-radius, high-visibility targets)")
if not top:
    print("  No non-test candidate modules found.")
else:
    for i, f in enumerate(top, 1):
        print(f"  {i}. {f['path']}")
        print(f"     {target_rationale(f)}")
print()
PYASSESS
        return $?
    fi

    if [ "$do_report" = "true" ]; then
        local heal_dir="${codebase_path:-.}/.loki/healing"
        if [[ ! -d "$heal_dir" ]]; then
            echo -e "${YELLOW}No healing session found.${NC}"
            return 0
        fi
        echo -e "${BOLD}Healing Report${NC}"
        echo ""
        echo "  Friction map:               $(_HEAL_FILE="$heal_dir/friction-map.json" python3 -c "import json, os; print(len(json.load(open(os.environ['_HEAL_FILE'])).get('frictions', [])))" 2>/dev/null || echo '0') points"
        echo "  Failure modes:              $(_HEAL_FILE="$heal_dir/failure-modes.json" python3 -c "import json, os; print(len(json.load(open(os.environ['_HEAL_FILE'])).get('modes', [])))" 2>/dev/null || echo '0') cataloged"
        echo "  Institutional knowledge:    $(wc -l < "$heal_dir/institutional-knowledge.md" 2>/dev/null || echo '0') lines"
        echo "  Characterization tests:     $(find "$heal_dir/characterization-tests/" -name "*.json" 2>/dev/null | wc -l | tr -d ' ') tests"
        echo ""
        return 0
    fi

    # Validate codebase path
    if [ -z "$codebase_path" ]; then
        echo -e "${RED}Error: Codebase path is required${NC}"
        echo "Usage: loki heal <path-to-codebase> [options]"
        return 2
    fi

    if [[ ! -d "$codebase_path" ]]; then
        echo -e "${RED}Error: Directory not found: $codebase_path${NC}"
        return 1
    fi

    # Validate phase
    case "$phase" in
        archaeology|stabilize|isolate|modernize|validate) ;;
        *)
            echo -e "${RED}Error: Invalid phase: $phase${NC}"
            echo "Valid phases: archaeology, stabilize, isolate, modernize, validate"
            return 1
            ;;
    esac

    # Initialize healing directory
    local heal_dir="$codebase_path/.loki/healing"
    mkdir -p "$heal_dir"/{behavioral-baseline,characterization-tests}

    # BUG-HEAL-004: Source migration hooks for healing enforcement
    local hooks_file
    hooks_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hooks/migration-hooks.sh"
    if [[ -f "$hooks_file" ]]; then
        # shellcheck source=hooks/migration-hooks.sh
        source "$hooks_file"
        load_migration_hook_config "$codebase_path"
    fi

    # Export healing environment variables for hooks
    export LOKI_HEAL_MODE="true"
    export LOKI_HEAL_PHASE="$phase"
    export LOKI_HEAL_STRICT="$strict"
    export LOKI_CODEBASE_PATH="$codebase_path"

    # Initialize healing state files if they don't exist
    [[ ! -f "$heal_dir/friction-map.json" ]] && echo '{"frictions":[]}' > "$heal_dir/friction-map.json"
    [[ ! -f "$heal_dir/failure-modes.json" ]] && echo '{"modes":[]}' > "$heal_dir/failure-modes.json"
    [[ ! -f "$heal_dir/institutional-knowledge.md" ]] && echo "# Institutional Knowledge Registry" > "$heal_dir/institutional-knowledge.md"

    # Initialize or update healing progress
    if [[ ! -f "$heal_dir/healing-progress.json" ]] || [ "$do_resume" != "true" ]; then
        # WAVE8 loki-est: pass codebase/phase/strict/out-path via env instead of
        # interpolating raw bash into the python source. A codebase path or phase
        # containing an apostrophe made this a SyntaxError; under `|| true` the
        # progress file was then silently never written (and the later
        # prev_phase read would fail), breaking healing resume.
        LOKI_HEAL_CODEBASE="$codebase_path" \
        LOKI_HEAL_PHASE_VAL="$phase" \
        LOKI_HEAL_STRICT_VAL="$strict" \
        LOKI_HEAL_OUT="$heal_dir/healing-progress.json" \
        python3 -c "
import json, os
from datetime import datetime
progress = {
    'codebase': os.environ.get('LOKI_HEAL_CODEBASE', ''),
    'started': datetime.now().isoformat(),
    'current_phase': os.environ.get('LOKI_HEAL_PHASE_VAL', ''),
    'strict_mode': os.environ.get('LOKI_HEAL_STRICT_VAL', '') == 'true',
    'components': [],
    'overall_health': 0.0
}
with open(os.environ['LOKI_HEAL_OUT'], 'w') as f:
    json.dump(progress, f, indent=2)
" || true
    fi

    # BUG-HEAL-004: Validate phase gate when resuming from a previous phase
    if [ "$do_resume" = "true" ] && [[ -f "$heal_dir/healing-progress.json" ]] && type hook_healing_phase_gate &>/dev/null; then
        local prev_phase
        prev_phase=$(LOKI_HEAL_PROG="$heal_dir/healing-progress.json" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_HEAL_PROG'])).get('current_phase', 'archaeology'))" 2>/dev/null || echo "archaeology")
        if [[ "$prev_phase" != "$phase" ]]; then
            local gate_result
            if ! gate_result=$(hook_healing_phase_gate "$prev_phase" "$phase" 2>&1); then
                echo -e "${RED}Phase gate check failed:${NC}"
                echo "  $gate_result"
                return 1
            fi
        fi
    fi

    emit_event healing cli start "phase=$phase" "codebase=$codebase_path" "strict=$strict" 2>/dev/null || true

    echo -e "${BOLD}Legacy System Healing${NC}"
    echo ""
    echo -e "  Codebase:   $codebase_path"
    echo -e "  Phase:      $phase"
    echo -e "  Strict:     $strict"
    echo -e "  Provider:   $provider"
    if [ "$archaeology_only" = "true" ]; then
        echo -e "  Mode:       archaeology-only (no modifications)"
    fi
    echo ""

    if [ "$dry_run" = "true" ]; then
        echo -e "${CYAN}Dry run: Showing healing plan without executing${NC}"
        echo ""
        echo "  Phase 1: Archaeology"
        echo "    - Map dependency graph"
        echo "    - Scan for friction points (sleeps, retries, magic values)"
        echo "    - Extract institutional knowledge from comments"
        echo "    - Write characterization tests for critical paths"
        echo ""
        echo "  Phase 2: Stabilize"
        echo "    - Add logging/observability without behavior changes"
        echo "    - Extract hardcoded config values"
        echo "    - Add type annotations where possible"
        echo ""
        echo "  Phase 3: Isolate"
        echo "    - Define component boundaries"
        echo "    - Create adapter interfaces"
        echo "    - Add integration tests at boundaries"
        echo ""
        echo "  Phase 4: Modernize"
        echo "    - Replace components one at a time behind adapters"
        echo "    - Verify characterization tests after each replacement"
        echo ""
        echo "  Phase 5: Validate"
        echo "    - Compare outputs with pre-healing baseline"
        echo "    - Generate healing report"
        return 0
    fi

    # Build healing prompt for the AI provider
    local heal_prompt="You are performing legacy system healing on the codebase at '${codebase_path}'.

IMPORTANT: Read skills/healing.md for the full healing protocol.

Current phase: ${phase}
Strict mode: ${strict}
Archaeology only: ${archaeology_only}

HEALING RULES:
1. FRICTION IS SEMANTICS: Before removing any 'quirky' code (sleeps, retries, magic values), verify it is not an undocumented business rule. Document all friction in .loki/healing/friction-map.json.
2. LEARN THROUGH FAILURE: Deliberately probe error paths and edge cases. Catalog every failure mode in .loki/healing/failure-modes.json. Each failure teaches you about the system.
3. CHARACTERIZE BEFORE MODIFYING: Write tests that capture CURRENT behavior (not intended behavior) before changing anything.
4. PRESERVE INSTITUTIONAL LOGIC: Extract business rules from comments, git history, error messages, and test fixtures into .loki/healing/institutional-knowledge.md.
5. INCREMENTAL ONLY: Change ONE component at a time. Verify ALL characterization tests pass after each change.

Phase-specific instructions:
- archaeology: Map dependencies, catalog friction, extract knowledge, write characterization tests. Do NOT modify source code. ALSO write .loki/healing/boundaries.json: a JSON array of the OBSERVABLE system boundaries (CLI commands, HTTP endpoints, file/DB-producing entrypoints). Each element: {\"id\": string, \"boundary_type\": \"cli\"|\"http\"|\"file\"|\"db\", \"command\": string}. The \"command\" MUST be a deterministic shell invocation that exercises that boundary and prints its observable output to stdout (avoid timestamps/PIDs). These commands are golden-mastered into .loki/healing/behavioral-baseline/ and re-run at the modernize->validate gate to block any undocumented boundary change. If no observable boundary exists, write an empty array [] (do NOT invent one).
- stabilize: Add logging and observability. Extract config. Add type hints. Do NOT change behavior.
- isolate: Create adapter interfaces at component boundaries. Add integration tests.
- modernize: Replace components behind adapters. Verify characterization tests pass.
- validate: Compare outputs with behavioral baseline. Generate healing report.

$([ "$strict" = "true" ] && echo "STRICT MODE: You MUST NOT change any observable behavior without creating a .loki/signals/HUMAN_REVIEW_NEEDED signal and waiting for approval.")
$([ "$archaeology_only" = "true" ] && echo "ARCHAEOLOGY ONLY: Do NOT modify any source files. Only create files in .loki/healing/.")

Begin healing now. Follow the RARV cycle for each action."

    # Execute with the appropriate provider
    local heal_exit=0
    # caveman ACTIVATION (free-form narration): heal execution writes its artifacts
    # (.loki/healing/*, characterization tests) via tools; stdout is piped to python
    # and only DISPLAYED, never captured/parsed for a verdict. Compress narration at
    # the configured level when warranted; empty -> bare branch, byte-identical to
    # pre-caveman (empty mode is NOT inert). No-op when caveman is absent.
    local _loki_heal_cm=""
    _loki_heal_cm="$(_loki_caveman_narration_level)"
    case "$provider" in
        claude)
            _loki_perm_argv_for "$codebase_path"
            local run_args=("${_LOKI_PERM_ARGV[@]}" -p "$heal_prompt" --output-format stream-json --verbose)
            (cd "$codebase_path" && if [ -n "$_loki_heal_cm" ]; then SHELL="$_LOKI_CLAUDE_SHELL" CAVEMAN_DEFAULT_MODE="$_loki_heal_cm" claude "${run_args[@]}" 2>&1; else SHELL="$_LOKI_CLAUDE_SHELL" claude "${run_args[@]}" 2>&1; fi) | \
                while IFS= read -r line; do
                    if echo "$line" | python3 -c "
import sys, json
try:
    d = json.loads(sys.stdin.read())
    if d.get('type') == 'assistant':
        for item in d.get('message', {}).get('content', []):
            if item.get('type') == 'text':
                print(item.get('text', ''), end='')
except Exception: pass
" 2>/dev/null; then
                        true
                    fi
                done && heal_exit=0 || heal_exit=$?
            ;;
        codex)
            (cd "$codebase_path" && codex exec --sandbox workspace-write "$heal_prompt" 2>&1) || heal_exit=$?
            ;;
        cline)
            (cd "$codebase_path" && cline -y "$heal_prompt" 2>&1) || heal_exit=$?
            ;;
        aider)
            local aider_model="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
            local aider_flags="${LOKI_AIDER_FLAGS:-}"
            # shellcheck disable=SC2086
            (cd "$codebase_path" && aider --message "$heal_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || heal_exit=$?
            ;;
        opencode)
            (cd "$codebase_path" && source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$heal_prompt" 2>&1) || heal_exit=$?
            ;;
        # BUG-HEAL-003: Unknown provider should error, not silently succeed
        *)
            echo -e "${RED}Error: Unknown provider: $provider${NC}"
            echo "Supported providers: claude, codex, cline, aider, opencode"
            return 1
            ;;
    esac

    emit_event healing cli complete "phase=$phase" "exit=$heal_exit" 2>/dev/null || true

    # RANK 4: populate .loki/healing/behavioral-baseline/ during archaeology.
    # The dir is mkdir'd above but was never populated; the modernize->validate
    # gate needs a golden-master per detected boundary to compare against.
    # Deterministic capture (NOT an LLM call): re-runs each boundary's
    # characterization_test and records observed stdout+exit. Honest degrade when
    # no boundaries are detected. Byte-identical no-op when the hook is absent.
    if [ "$phase" = "archaeology" ] && type hook_capture_behavioral_baseline &>/dev/null; then
        hook_capture_behavioral_baseline "$codebase_path" 2>/dev/null || true
    fi

    if [ "$heal_exit" -eq 0 ]; then
        echo ""
        echo -e "${GREEN}Healing phase '${phase}' complete.${NC}"
        echo -e "View progress: loki heal --status ${codebase_path}"
        echo -e "View friction:  loki heal --friction-map ${codebase_path}"
    else
        echo ""
        echo -e "${YELLOW}Healing phase '${phase}' finished with warnings (exit: $heal_exit).${NC}"
        echo -e "Check: loki heal --status ${codebase_path}"
    fi
}

cmd_migrate() {
    local codebase_path=""
    local target=""
    local plan_only="false"
    local show_plan="false"
    local phase=""
    local parallel=""
    local compliance=""
    local dry_run="false"
    local do_resume="false"
    local multi_repo=""
    local export_report="false"
    local do_list="false"
    local do_status="false"
    local status_id=""
    local no_dashboard="false"
    local no_docs="false"

    if [ $# -eq 0 ]; then
        cmd_migrate_help
        return 0
    fi

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                cmd_migrate_help
                return 0
                ;;
            --list)
                do_list="true"
                shift
                ;;
            --status)
                do_status="true"
                if [[ -n "${2:-}" ]] && [[ ! "$2" =~ ^-- ]]; then
                    status_id="$2"
                    shift
                fi
                shift
                ;;
            --show-plan)
                show_plan="true"
                shift
                ;;
            --target)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --target requires a value${NC}"
                    return 1
                fi
                target="$2"
                shift 2
                ;;
            --target=*)
                target="${1#*=}"
                shift
                ;;
            --plan-only)
                plan_only="true"
                shift
                ;;
            --phase)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --phase requires a value${NC}"
                    return 1
                fi
                phase="$2"
                shift 2
                ;;
            --phase=*)
                phase="${1#*=}"
                shift
                ;;
            --parallel)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --parallel requires a value${NC}"
                    return 1
                fi
                parallel="$2"
                shift 2
                ;;
            --parallel=*)
                parallel="${1#*=}"
                shift
                ;;
            --compliance)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --compliance requires a value${NC}"
                    return 1
                fi
                compliance="$2"
                shift 2
                ;;
            --compliance=*)
                compliance="${1#*=}"
                shift
                ;;
            --dry-run)
                dry_run="true"
                shift
                ;;
            --resume)
                do_resume="true"
                shift
                ;;
            --multi-repo)
                if [[ -z "${2:-}" ]]; then
                    echo -e "${RED}Error: --multi-repo requires a value${NC}"
                    return 1
                fi
                multi_repo="$2"
                shift 2
                ;;
            --multi-repo=*)
                multi_repo="${1#*=}"
                shift
                ;;
            --export-report)
                export_report="true"
                shift
                ;;
            --no-dashboard)
                no_dashboard="true"
                shift
                ;;
            --no-docs)
                no_docs="true"
                shift
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki migrate --help' for usage."
                return 1
                ;;
            *)
                if [ -z "$codebase_path" ]; then
                    codebase_path="$1"
                else
                    echo -e "${RED}Error: Unexpected argument: $1${NC}"
                    echo "Run 'loki migrate --help' for usage."
                    return 1
                fi
                shift
                ;;
        esac
    done

    # Route to subcommands
    if [ "$do_list" = "true" ]; then
        cmd_migrate_list
        return 0
    fi

    if [ "$do_status" = "true" ]; then
        cmd_migrate_status "$status_id"
        return 0
    fi

    if [ "$show_plan" = "true" ]; then
        cmd_migrate_show_plan "${codebase_path:-$(pwd)}"
        return 0
    fi

    # Default: start or continue migration
    if [ -z "$codebase_path" ]; then
        echo -e "${RED}Error: Codebase path is required${NC}"
        echo "Usage: loki migrate <path-to-codebase> --target <target>"
        echo ""
        echo "Run 'loki migrate --help' for usage."
        return 2
    fi

    cmd_migrate_start "$codebase_path" "$target" "$plan_only" "$phase" "$parallel" "$compliance" "$dry_run" "$do_resume" "$multi_repo" "$export_report" "$no_dashboard" "$no_docs"
}

#===============================================================================
# loki cluster - Custom workflow templates (v6.2.0)
#===============================================================================

cmd_cluster() {
    local subcmd="${1:-list}"
    shift 2>/dev/null || true

    case "$subcmd" in
        --help|-h|help)
            echo -e "${BOLD}loki cluster${NC} - Custom workflow templates"
            echo ""
            echo "Usage: loki cluster <command> [options]"
            echo ""
            echo "Commands:"
            echo "  list               List available workflow templates"
            echo "  validate <name>    Validate a cluster template"
            echo "  run <name> [args]  Execute a cluster workflow"
            echo "  info <name>        Show template details"
            echo ""
            echo "Options for 'run':"
            echo "  --cluster-id <id>  Named cluster ID for crash recovery"
            echo "  --resume           Resume a previously crashed cluster run"
            echo ""
            echo "Examples:"
            echo "  loki cluster list"
            echo "  loki cluster validate security-review"
            echo "  loki cluster info code-review"
            echo "  loki cluster run security-review --cluster-id my-review"
            echo "  loki cluster run security-review --cluster-id my-review --resume"
            ;;
        list)
            echo -e "${BOLD}Available Cluster Templates${NC}"
            echo ""
            local template_dir="$SKILL_DIR/templates/clusters"
            if [[ ! -d "$template_dir" ]]; then
                echo "No templates found at $template_dir"
                return 1
            fi
            for f in "$template_dir"/*.json; do
                [[ ! -f "$f" ]] && continue
                local name
                name=$(basename "$f" .json)
                local desc
                desc=$(python3 -c "
import json, sys
try:
    with open(sys.argv[1]) as f:
        d = json.load(f)
    print(d.get('description', 'No description'))
except: print('Error reading template')
" "$f" 2>/dev/null || echo "")
                local agent_count
                agent_count=$(python3 -c "
import json, sys
try:
    with open(sys.argv[1]) as f:
        d = json.load(f)
    print(len(d.get('agents', [])))
except: print('?')
" "$f" 2>/dev/null || echo "?")
                printf "  %-20s %s agents  %s\n" "$name" "$agent_count" "$desc"
            done
            ;;
        validate)
            local template_name="${1:-}"
            if [[ -z "$template_name" ]]; then
                echo -e "${RED}Usage: loki cluster validate <template-name>${NC}"
                return 1
            fi
            local template_file="$SKILL_DIR/templates/clusters/${template_name}.json"
            if [[ ! -f "$template_file" ]]; then
                echo -e "${RED}Template not found: $template_name${NC}"
                echo "Run 'loki cluster list' to see available templates."
                return 1
            fi
            echo -e "${CYAN}Validating: $template_name${NC}"
            local errors
            errors=$(python3 -c "
import json, sys
sys.path.insert(0, '$SKILL_DIR')
from swarm.patterns import TopologyValidator
errors = TopologyValidator.validate_file(sys.argv[1])
if errors:
    for e in errors:
        print(f'ERROR: {e}')
else:
    print('VALID')
" "$template_file" 2>&1)
            if echo "$errors" | grep -q "^VALID$"; then
                echo -e "${GREEN}Template is valid${NC}"
                return 0
            else
                echo -e "${RED}Validation errors:${NC}"
                echo "$errors"
                return 1
            fi
            ;;
        info)
            local template_name="${1:-}"
            if [[ -z "$template_name" ]]; then
                echo -e "${RED}Usage: loki cluster info <template-name>${NC}"
                return 1
            fi
            local template_file="$SKILL_DIR/templates/clusters/${template_name}.json"
            if [[ ! -f "$template_file" ]]; then
                echo -e "${RED}Template not found: $template_name${NC}"
                return 1
            fi
            python3 -c "
import json, sys
with open(sys.argv[1]) as f:
    d = json.load(f)
print(f\"Name:        {d.get('name', 'unknown')}\")
print(f\"Description: {d.get('description', 'none')}\")
print(f\"Version:     {d.get('version', 'unknown')}\")
print(f\"Topology:    {d.get('topology', 'unknown')}\")
print(f\"Agents:      {len(d.get('agents', []))}\")
print()
for a in d.get('agents', []):
    subs = ', '.join(a.get('subscribes', []))
    pubs = ', '.join(a.get('publishes', []))
    print(f\"  [{a['id']}] ({a.get('type', '?')})\")
    print(f\"    Role: {a.get('role', '?')}\")
    print(f\"    Subscribes: {subs}\")
    print(f\"    Publishes:  {pubs}\")
    print()
" "$template_file"
            ;;
        run)
            local template_name="${1:-}"
            shift 2>/dev/null || true
            if [[ -z "$template_name" ]]; then
                echo -e "${RED}Usage: loki cluster run <template-name> [--cluster-id ID] [--resume]${NC}"
                return 1
            fi
            local template_file="$SKILL_DIR/templates/clusters/${template_name}.json"
            if [[ ! -f "$template_file" ]]; then
                echo -e "${RED}Template not found: $template_name${NC}"
                return 1
            fi

            # Parse run options
            local cluster_id="" do_resume="false"
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --cluster-id) cluster_id="$2"; shift 2 ;;
                    --cluster-id=*) cluster_id="${1#*=}"; shift ;;
                    --resume) do_resume="true"; shift ;;
                    *) shift ;;
                esac
            done

            # Auto-generate cluster ID if not provided
            if [[ -z "$cluster_id" ]]; then
                cluster_id="${template_name}_$(date +%Y%m%d_%H%M%S)"
            fi

            # Validate first
            local result
            result=$(python3 -c "
import json, sys
sys.path.insert(0, '$SKILL_DIR')
from swarm.patterns import TopologyValidator
errors = TopologyValidator.validate_file(sys.argv[1])
if errors:
    for e in errors: print(f'ERROR: {e}')
else:
    print('VALID')
" "$template_file" 2>&1)
            if ! echo "$result" | grep -q "^VALID$"; then
                echo -e "${RED}Template validation failed:${NC}"
                echo "$result"
                return 1
            fi

            # Fire lifecycle hooks and record state.
            # WAVE10: pass cluster_id / template_name / template_file / SKILL_DIR
            # via os.environ instead of interpolating into the python source.
            # A --cluster-id containing a single quote (e.g. "o'brien") made the
            # heredoc body a SyntaxError, skipping all state recording while the
            # green "Cluster: ..." line still printed (silent false success).
            _LOKI_SKILL_DIR="${SKILL_DIR:-.}" \
            _LOKI_TEMPLATE_FILE="${template_file}" \
            _LOKI_CLUSTER_ID="${cluster_id}" \
            _LOKI_TEMPLATE_NAME="${template_name}" \
            _LOKI_DO_RESUME="${do_resume}" \
            PYTHONPATH="${SKILL_DIR:-.}" python3 -c '
import json, sys, os
skill_dir = os.environ["_LOKI_SKILL_DIR"]
sys.path.insert(0, skill_dir)
from swarm.patterns import ClusterLifecycleHooks
from state.sqlite_backend import SqliteStateBackend

# Load template hooks config
with open(os.environ["_LOKI_TEMPLATE_FILE"]) as f:
    tpl = json.load(f)

hooks = ClusterLifecycleHooks(tpl.get("hooks", {}))
db = SqliteStateBackend()

cluster_id = os.environ["_LOKI_CLUSTER_ID"]
template_name = os.environ["_LOKI_TEMPLATE_NAME"]
resume = os.environ["_LOKI_DO_RESUME"] == "true"

if resume:
    events = db.query_events(event_type="cluster_state", migration_id=cluster_id, limit=1)
    if events:
        print(f"Resuming cluster {cluster_id} from last checkpoint")
    else:
        print(f"No previous state found for {cluster_id}. Starting fresh.")

# Fire pre_run hooks
results = hooks.fire("pre_run", {"cluster_id": cluster_id, "template": template_name})
for r in results:
    if not r["success"]:
        print("Pre-run hook failed: " + str(r.get("output", "")))

# Record cluster start
db.record_event("cluster_start", {
    "cluster_id": cluster_id,
    "template": template_name,
    "agents": len(tpl.get("agents", [])),
    "resume": resume,
}, migration_id=cluster_id)

_n_agents = len(tpl.get("agents", []))
print(f"Cluster {cluster_id} initialized with {_n_agents} agents")
print("Template validated. Lifecycle hooks active.")
' 2>&1

            echo -e "${GREEN}Cluster: $cluster_id${NC}"
            echo -e "Template: $template_name"
            if [[ "$do_resume" == "true" ]]; then
                echo -e "Mode: resume"
            fi
            echo -e "Use 'loki state query events --migration $cluster_id' to inspect state."
            ;;
        *)
            echo -e "${RED}Unknown cluster command: $subcmd${NC}"
            echo "Run 'loki cluster --help' for usage."
            return 1
            ;;
    esac
}

# Event-driven trigger management (v6.17.0)
cmd_trigger() {
    local subcmd="${1:-help}"
    shift || true

    local trigger_server_script
    trigger_server_script="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/trigger-server.py"
    local trigger_schedule_script
    trigger_schedule_script="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/trigger-schedule.py"
    local pid_file=".loki/triggers/server.pid"

    case "$subcmd" in
        start)
            local port=""
            local secret=""
            local dry_run_flag=""
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --port) port="$2"; shift 2 ;;
                    --port=*) port="${1#--port=}"; shift ;;
                    --secret) secret="$2"; shift 2 ;;
                    --secret=*) secret="${1#--secret=}"; shift ;;
                    --dry-run) dry_run_flag="--dry-run"; shift ;;
                    *) shift ;;
                esac
            done
            if [[ -f "$pid_file" ]]; then
                local existing_pid
                existing_pid=$(cat "$pid_file" 2>/dev/null)
                if kill -0 "$existing_pid" 2>/dev/null; then
                    echo "Trigger server already running (pid=$existing_pid)"
                    return 0
                fi
            fi
            local args=()
            [[ -n "$port" ]] && args+=("--port" "$port")
            [[ -n "$secret" ]] && args+=("--secret" "$secret")
            [[ -n "$dry_run_flag" ]] && args+=("--dry-run")
            mkdir -p .loki/triggers
            # v7.7.13 fix: safe empty-array expansion (bash 3.2 + set -u).
            nohup python3 "$trigger_server_script" ${args[@]+"${args[@]}"} \
                > .loki/triggers/server.log 2>&1 &
            local server_pid=$!
            echo "$server_pid" > "$pid_file"
            sleep 0.5
            if kill -0 "$server_pid" 2>/dev/null; then
                echo "Trigger server started (pid=$server_pid)"
                local p="${port:-7373}"
                echo "Webhook endpoint: POST http://localhost:$p/webhook"
            else
                echo -e "${RED}Trigger server failed to start. Check .loki/triggers/server.log${NC}"
                return 1
            fi
            ;;
        stop)
            if [[ ! -f "$pid_file" ]]; then
                echo "Trigger server not running (no pid file)"
                return 0
            fi
            local pid
            pid=$(cat "$pid_file" 2>/dev/null)
            if [[ -z "$pid" ]]; then
                echo "Trigger server not running"
                rm -f "$pid_file"
                return 0
            fi
            if kill "$pid" 2>/dev/null; then
                echo "Trigger server stopped (pid=$pid)"
                rm -f "$pid_file"
            else
                echo "Trigger server was not running (pid=$pid)"
                rm -f "$pid_file"
            fi
            ;;
        status)
            if [[ -f "$pid_file" ]]; then
                local pid
                pid=$(cat "$pid_file" 2>/dev/null)
                if kill -0 "$pid" 2>/dev/null; then
                    echo "Trigger server: running (pid=$pid)"
                    if command -v curl >/dev/null 2>&1; then
                        local config_port
                        config_port=$(python3 -c "
import json, pathlib
p = pathlib.Path('.loki/triggers/config.json')
print(json.loads(p.read_text()).get('port', 7373) if p.exists() else 7373)
" 2>/dev/null || echo "7373")
                        curl -sf "http://localhost:$config_port/status" 2>/dev/null | \
                            python3 -m json.tool 2>/dev/null || true
                    fi
                else
                    echo "Trigger server: stopped (stale pid=$pid)"
                    rm -f "$pid_file"
                fi
            else
                echo "Trigger server: not running"
            fi
            local sched_file=".loki/triggers/schedules.json"
            if [[ -f "$sched_file" ]]; then
                local count
                count=$(_SCHED_FILE="$sched_file" python3 -c "import json, os; d=json.load(open(os.environ['_SCHED_FILE'])); print(len(d) if isinstance(d,list) else len(d.get('schedules',[])))" 2>/dev/null || echo "?")
                echo "Schedules configured: $count"
            else
                echo "Schedules configured: 0"
            fi
            ;;
        list)
            python3 "$trigger_schedule_script" list
            ;;
        add)
            local add_type="${1:-}"
            shift || true
            if [[ "$add_type" == "schedule" ]]; then
                local name="${1:-}"
                local cron_expr="${2:-}"
                local action="${3:-}"
                shift 3 || true
                if [[ -z "$name" || -z "$cron_expr" || -z "$action" ]]; then
                    echo "Usage: loki trigger add schedule <name> <cron_expr> <action> [args...]"
                    echo ""
                    echo "Example:"
                    echo "  loki trigger add schedule daily-review '0 9 * * 1-5' quality-review"
                    echo "  loki trigger add schedule auto-issue '*/30 * * * *' run 42"
                    return 1
                fi
                python3 "$trigger_schedule_script" add "$name" "$cron_expr" "$action" "$@"
            else
                echo "Usage: loki trigger add schedule <name> <cron_expr> <action> [args...]"
                return 1
            fi
            ;;
        remove)
            local name="${1:-}"
            if [[ -z "$name" ]]; then
                echo "Usage: loki trigger remove <name>"
                return 1
            fi
            python3 "$trigger_schedule_script" remove "$name"
            ;;
        daemon)
            local dry_run_flag=""
            [[ "${1:-}" == "--dry-run" ]] && dry_run_flag="--dry-run"
            python3 "$trigger_schedule_script" daemon $dry_run_flag
            ;;
        test)
            local event_type="${1:-issues}"
            shift || true
            local repo=""
            local issue_num=""
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --repo) repo="$2"; shift 2 ;;
                    --repo=*) repo="${1#--repo=}"; shift ;;
                    --issue) issue_num="$2"; shift 2 ;;
                    --issue=*) issue_num="${1#--issue=}"; shift ;;
                    *) shift ;;
                esac
            done
            local config_port
            config_port=$(python3 -c "
import json, pathlib
p = pathlib.Path('.loki/triggers/config.json')
print(json.loads(p.read_text()).get('port', 7373) if p.exists() else 7373)
" 2>/dev/null || echo "7373")

            local payload
            case "$event_type" in
                issues)
                    local num="${issue_num:-1}"
                    local r="${repo:-owner/repo}"
                    payload="{\"action\":\"opened\",\"issue\":{\"number\":$num,\"title\":\"Test issue\"},\"repository\":{\"full_name\":\"$r\"}}"
                    ;;
                pull_request)
                    local num="${issue_num:-1}"
                    local r="${repo:-owner/repo}"
                    payload="{\"action\":\"synchronize\",\"pull_request\":{\"number\":$num,\"title\":\"Test PR\"},\"repository\":{\"full_name\":\"$r\"}}"
                    ;;
                workflow_run)
                    local r="${repo:-owner/repo}"
                    payload="{\"action\":\"completed\",\"workflow_run\":{\"name\":\"CI\",\"conclusion\":\"failure\"},\"repository\":{\"full_name\":\"$r\"}}"
                    ;;
                *)
                    echo "Supported event types: issues, pull_request, workflow_run"
                    return 1
                    ;;
            esac

            if command -v curl >/dev/null 2>&1; then
                echo "Sending test $event_type event to http://localhost:$config_port/webhook"
                curl -sf -X POST \
                    -H "Content-Type: application/json" \
                    -H "X-GitHub-Event: $event_type" \
                    -d "$payload" \
                    "http://localhost:$config_port/webhook" | python3 -m json.tool 2>/dev/null || \
                    echo "Server not running or request failed. Start with: loki trigger start"
            else
                echo "curl not available. Cannot send test event."
                return 1
            fi
            ;;
        logs)
            local tail_n="50"
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --tail) tail_n="$2"; shift 2 ;;
                    --tail=*) tail_n="${1#--tail=}"; shift ;;
                    *) shift ;;
                esac
            done
            local log_file=".loki/triggers/events.log"
            if [[ -f "$log_file" ]]; then
                tail -n "$tail_n" "$log_file" | python3 -c "
import sys, json
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    try:
        e = json.loads(line)
        print('[%s] %-15s %-12s %-8s %s' % (
            e.get('timestamp','?'),
            e.get('event','?'),
            e.get('action','?'),
            e.get('status','?'),
            e.get('summary',''),
        ))
    except Exception:
        print(line)
" 2>/dev/null || cat "$log_file" | tail -n "$tail_n"
            else
                echo "No events logged yet. (.loki/triggers/events.log does not exist)"
            fi
            local server_log=".loki/triggers/server.log"
            if [[ -f "$server_log" ]]; then
                echo ""
                echo "--- Server log (last $tail_n lines) ---"
                tail -n "$tail_n" "$server_log"
            fi
            ;;
        help|--help|-h)
            echo -e "${BOLD}loki trigger${NC} - Event-driven autonomous execution"
            echo ""
            echo "Usage: loki trigger <subcommand> [options]"
            echo ""
            echo "Subcommands:"
            echo "  start [--port PORT] [--secret SECRET] [--dry-run]"
            echo "                        Start the webhook receiver server (default port: 7373)"
            echo "  stop                  Stop the webhook receiver server"
            echo "  status                Show server and schedule status"
            echo "  list                  List configured schedules"
            echo "  add schedule <name> <cron_expr> <action> [args...]"
            echo "                        Add a cron-based schedule trigger"
            echo "  remove <name>         Remove a schedule trigger"
            echo "  daemon [--dry-run]    Run one pass of schedule checks (call from cron)"
            echo "  test <event-type> [--repo owner/repo] [--issue NUM]"
            echo "                        Send a test webhook event (event-types: issues, pull_request, workflow_run)"
            echo "  logs [--tail N]       Show recent trigger event log (default: last 50)"
            echo "  help                  Show this help"
            echo ""
            echo "Webhook events handled:"
            echo "  issues (opened)        -> loki run <issue_number> --pr --detach"
            echo "  pull_request (sync)    -> loki run <pr_number> --detach"
            echo "  workflow_run (failure) -> loki run --detach"
            echo ""
            echo "Schedule cron format: minute hour day-of-month month day-of-week"
            echo "  Examples:"
            echo "    '0 9 * * 1-5'   - 9am Mon-Fri"
            echo "    '*/30 * * * *'  - Every 30 minutes"
            echo "    '0 0 * * 0'     - Midnight Sunday"
            echo ""
            echo "Examples:"
            echo "  loki trigger start --port 7373 --secret mywebhooksecret"
            echo "  loki trigger add schedule daily-review '0 9 * * 1-5' quality-review"
            echo "  loki trigger test issues --repo owner/repo --issue 42"
            echo "  loki trigger logs --tail 20"
            echo "  loki trigger stop"
            ;;
        *)
            echo -e "${RED}Unknown trigger command: $subcmd${NC}"
            echo "Run 'loki trigger help' for usage."
            return 1
            ;;
    esac
}

# Cross-provider auto-failover management (v6.19.0)
cmd_failover() {
    local loki_dir="${TARGET_DIR:-.}/.loki"
    local failover_file="$loki_dir/state/failover.json"
    local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

    if [ $# -eq 0 ]; then
        # No args: show current failover config and health
        if [ ! -f "$failover_file" ]; then
            echo -e "${BOLD}Cross-Provider Auto-Failover${NC}"
            echo ""
            echo "Status: DISABLED"
            echo ""
            echo "Enable with: loki failover --enable"
            echo "Or set:      LOKI_FAILOVER=true loki start"
            return 0
        fi

        echo -e "${BOLD}Cross-Provider Auto-Failover${NC}"
        echo ""
        python3 << 'PYEOF'
import json, os
fpath = os.path.join(os.environ.get('TARGET_DIR', '.'), '.loki/state/failover.json')
try:
    with open(fpath) as f:
        d = json.load(f)
    status = "ENABLED" if d.get("enabled") else "DISABLED"
    print(f"Status:           {status}")
    print(f"Chain:            {' -> '.join(d.get('chain', []))}")
    print(f"Current provider: {d.get('currentProvider', 'unknown')}")
    print(f"Primary provider: {d.get('primaryProvider', 'unknown')}")
    print(f"Failover count:   {d.get('failoverCount', 0)}")
    last = d.get('lastFailover')
    print(f"Last failover:    {last if last else 'never'}")
    print()
    print("Health Status:")
    health = d.get('healthCheck', {})
    for provider in d.get('chain', []):
        h = health.get(provider, 'unknown')
        indicator = '[OK]' if h == 'healthy' else '[--]' if h == 'unknown' else '[!!]'
        print(f"  {indicator} {provider}: {h}")
except FileNotFoundError:
    print("Failover not initialized. Run: loki failover --enable")
except Exception as e:
    print(f"Error reading failover state: {e}")
PYEOF
        return 0
    fi

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --enable)
                mkdir -p "$loki_dir/state"
                local current_provider
                current_provider=$(cat "$loki_dir/state/provider" 2>/dev/null || echo "claude")
                local chain="${LOKI_FAILOVER_CHAIN:-claude,codex,cline}"

                cat > "$failover_file" << FEOF
{
  "enabled": true,
  "chain": $(printf '%s' "$chain" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip().split(",")))' 2>/dev/null || echo '["claude","codex","cline"]'),
  "currentProvider": "$current_provider",
  "primaryProvider": "$current_provider",
  "lastFailover": null,
  "failoverCount": 0,
  "healthCheck": {}
}
FEOF
                echo -e "${GREEN}Failover enabled${NC}"
                echo "Chain: $chain"
                echo "Primary: $current_provider"
                shift
                ;;
            --disable)
                if [ -f "$failover_file" ]; then
                    if _FAILOVER_FILE="$failover_file" python3 -c "
import json, os
failover_file = os.environ['_FAILOVER_FILE']
with open(failover_file) as f: d = json.load(f)
d['enabled'] = False
with open(failover_file, 'w') as f: json.dump(d, f, indent=2)
"; then
                        echo -e "${YELLOW}Failover disabled${NC}"
                    else
                        echo -e "${RED}Error: failed to disable failover${NC}"
                        return 1
                    fi
                else
                    echo "Failover not initialized."
                fi
                shift
                ;;
            --chain)
                shift
                local new_chain="$1"
                if [ -z "$new_chain" ]; then
                    echo -e "${RED}Error: --chain requires a comma-separated list of providers${NC}"
                    return 1
                fi
                # Validate each provider in chain
                local IFS=','
                for p in $new_chain; do
                    case "$p" in
                        claude|codex|cline|aider|opencode) ;;
                        *) echo -e "${RED}Error: invalid provider '$p' in chain${NC}"; return 1 ;;
                    esac
                done
                unset IFS

                if [ ! -f "$failover_file" ]; then
                    echo -e "${RED}Error: failover not initialized. Run: loki failover --enable${NC}"
                    return 1
                fi

                if _FAILOVER_FILE="$failover_file" _NEW_CHAIN="$new_chain" python3 -c "
import json, os
failover_file = os.environ['_FAILOVER_FILE']
new_chain = os.environ['_NEW_CHAIN']
with open(failover_file) as f: d = json.load(f)
d['chain'] = new_chain.split(',')
with open(failover_file, 'w') as f: json.dump(d, f, indent=2)
"; then
                    echo "Failover chain updated: $new_chain"
                else
                    echo -e "${RED}Error: failed to update failover chain${NC}"
                    return 1
                fi
                shift
                ;;
            --test)
                echo -e "${BOLD}Testing all providers in failover chain...${NC}"
                echo ""
                # Source provider loader for health checks
                if [ -f "$script_dir/../providers/loader.sh" ]; then
                    source "$script_dir/../providers/loader.sh"
                fi

                local chain_providers="claude,codex,cline"
                if [ -f "$failover_file" ]; then
                    chain_providers=$(_FAILOVER_FILE="$failover_file" python3 -c "import json, os; print(','.join(json.load(open(os.environ['_FAILOVER_FILE'])).get('chain', ['claude','codex','cline'])))" 2>/dev/null || echo "claude,codex,cline")
                fi

                local IFS=','
                local all_pass=true
                for p in $chain_providers; do
                    printf "  %-10s " "$p:"

                    # Check CLI installed
                    local cli_name="$p"
                    if command -v "$cli_name" &>/dev/null; then
                        local cli_version
                        cli_version=$("$cli_name" --version 2>/dev/null | head -1 || echo "unknown")
                        printf "CLI %-20s " "[$cli_version]"
                    else
                        printf "CLI %-20s " "[NOT INSTALLED]"
                        echo -e "${RED}FAIL${NC}"
                        all_pass=false
                        continue
                    fi

                    # Check API key
                    local has_key=false
                    case "$p" in
                        claude) [ -n "${ANTHROPIC_API_KEY:-}" ] && has_key=true ;;
                        codex) [ -n "${OPENAI_API_KEY:-}" ] && has_key=true ;;
                        cline|aider) has_key=true ;;  # Key check varies
                    esac

                    if [ "$has_key" = "true" ]; then
                        printf "Key [OK]  "
                        echo -e "${GREEN}PASS${NC}"
                    else
                        printf "Key [MISSING]  "
                        echo -e "${RED}FAIL${NC}"
                        all_pass=false
                    fi
                done
                unset IFS

                echo ""
                if [ "$all_pass" = "true" ]; then
                    echo -e "${GREEN}All providers healthy${NC}"
                else
                    echo -e "${YELLOW}Some providers unavailable - failover chain may be limited${NC}"
                fi
                shift
                ;;
            --reset)
                if [ -f "$failover_file" ]; then
                    rm -f "$failover_file"
                    echo "Failover state reset to defaults."
                else
                    echo "No failover state to reset."
                fi
                shift
                ;;
            --help|-h)
                echo -e "${BOLD}loki failover${NC} - Cross-provider auto-failover management"
                echo ""
                echo "Usage: loki failover [options]"
                echo ""
                echo "Options:"
                echo "  (no args)        Show failover status and health"
                echo "  --enable         Enable auto-failover"
                echo "  --disable        Disable auto-failover"
                echo "  --chain X,Y,Z    Set failover chain (e.g., claude,codex,cline)"
                echo "  --test           Test all providers in chain"
                echo "  --reset          Reset failover state to defaults"
                echo "  --help, -h       Show this help"
                echo ""
                echo "Environment:"
                echo "  LOKI_FAILOVER=true         Enable failover at startup"
                echo "  LOKI_FAILOVER_CHAIN=X,Y,Z  Set default chain"
                echo ""
                echo "When a rate limit (429/529) is detected, Loki automatically switches"
                echo "to the next healthy provider in the chain. After each successful"
                echo "iteration on a fallback provider, the primary is health-checked and"
                echo "execution switches back when it recovers."
                return 0
                ;;
            *)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki failover --help' for usage."
                return 1
                ;;
        esac
    done
}

# Reusable PRD plan analysis (v6.81.1)
# Shared by `loki plan`, `loki start` (auto-plan), and `loki run` (auto-plan).
# Arguments:
#   $1 - absolute PRD path
#   $2 - show_json (true|false)
#   $3 - show_verbose (true|false)
# Respects:
#   LOKI_SESSION_MODEL          - Pins ALL iterations to one tier (default: sonnet)
#   LOKI_LEGACY_TIER_SWITCHING  - When 'true', restores per-iteration rotation
show_prd_plan() {
    local prd_path="$1"
    local show_json="${2:-false}"
    local show_verbose="${3:-false}"

    if [ -z "$prd_path" ] || [ ! -f "$prd_path" ]; then
        echo -e "${RED}show_prd_plan: PRD file not found: $prd_path${NC}" >&2
        return 1
    fi

    python3 -c "
import json, sys, os, re, math

prd_path = sys.argv[1]
show_json = sys.argv[2] == 'true'
show_verbose = sys.argv[3] == 'true'

# v6.81.1: Respect LOKI_SESSION_MODEL (pinned by v6.81.0) so the estimate
# matches what the main loop actually invokes. LOKI_LEGACY_TIER_SWITCHING=true
# restores the legacy per-iteration Opus/Sonnet/Haiku rotation for users who
# explicitly opt out.
session_model_env = (os.environ.get('LOKI_SESSION_MODEL', 'sonnet') or 'sonnet').strip().lower()
legacy_tier_switching = (os.environ.get('LOKI_LEGACY_TIER_SWITCHING', 'false') or 'false').strip().lower() == 'true'

# Model-honesty contract: the estimator must quote the model the runner will
# actually invoke, and name the lever that selected it. The runner honors only
# two session-level levers (LOKI_MODEL was estimator-only and is removed):
#   1. A pending mid-flight override file (.loki/state/model-override) -- the
#      most specific, just-requested intent; it wins.
#   2. LOKI_SESSION_MODEL (the session pin the runner reads at run.sh:12309).
# Canonical normalization is shared with run.sh + the dashboard: trim +
# lowercase + EXACT allowlist match. A value with interior whitespace (fab le)
# is rejected everywhere, so the three readers agree.
def _loki_norm_alias(raw):
    raw = (raw or '').strip().lower()
    return raw if raw in ('haiku', 'sonnet', 'opus', 'fable') else ''

# Session-pin normalization is BROADER than the override-file allowlist above.
# run.sh's session-pin case (run.sh:12331) accepts the four model aliases AND
# the three raw tier names (planning|development|fast) -- documented at
# skills/model-selection.md:8 -- mapping each through resolve_model_for_tier.
# The OVERRIDE file path (_loki_norm_alias) stays narrow because that value is
# fed straight to 'claude --model', where tier names are not valid. The session
# pin is a tier route, so tier names ARE valid pins. This normalizer mirrors
# run.sh's trim+lowercase (interior whitespace preserved, so 'fab le' stays junk
# and falls through to the default tier exactly like the runner's '*' arm).
#
# GENERIC TIER VOCABULARY (small|medium|high): translated onto the canonical
# tier names before the allowlist check, mirroring run.sh's entry-point case.
# The estimator runs in its own process and reads LOKI_SESSION_MODEL directly,
# so without this a 'medium' pin would fall through to the '' default and the
# quote would silently price a different model than the run dispatches -- the
# exact estimator-vs-runner divergence this file keeps paying down. Only the
# three new words are translated; every existing value passes through unchanged.
_LOKI_GENERIC_TIERS = {'small': 'fast', 'medium': 'development', 'high': 'planning'}

def _loki_norm_session_pin(raw):
    raw = (raw or '').strip().lower()
    raw = _LOKI_GENERIC_TIERS.get(raw, raw)
    return raw if raw in (
        'haiku', 'sonnet', 'opus', 'fable',
        'planning', 'development', 'fast',
    ) else ''

# session_model_env starts from LOKI_SESSION_MODEL; track the lever that set it
# so the provenance line names the real source (never a false attribution).
_session_model_source = 'LOKI_SESSION_MODEL'
session_model_env = _loki_norm_session_pin(os.environ.get('LOKI_SESSION_MODEL', '')) or 'sonnet'

_fable_override = ''
try:
    _ov_path = os.path.join('.loki', 'state', 'model-override')
    if os.path.isfile(_ov_path):
        with open(_ov_path) as _ovf:
            _fable_override = _loki_norm_alias(_ovf.read())
except Exception:
    _fable_override = ''
if _fable_override:
    session_model_env = _fable_override
    _session_model_source = '.loki/state/model-override'

# Architect opt-in (LOKI_FABLE_ARCHITECT=1): the runner routes ONLY the first
# (architecture) iteration to fable. Disclose that here so the quote is not
# blind to the lever (it would otherwise under-quote by one fable iteration).
# Suppressed when an explicit planning-model override or a fable session/override
# already applies. The maxTier clamp below still caps the disclosed cost.
_fable_architect = (
    (os.environ.get('LOKI_FABLE_ARCHITECT', '0') or '0').strip() == '1'
    and not os.environ.get('LOKI_CLAUDE_MODEL_PLANNING')
    and not os.environ.get('LOKI_MODEL_PLANNING')
    and session_model_env != 'fable'
    and not (os.environ.get('LOKI_LEGACY_TIER_SWITCHING', 'false') or 'false').strip().lower() == 'true'
)

# Apply the SAME LOKI_MAX_TIER ceiling the runner applies, so the quoted model
# (and the architect disclosure) never exceed the operator cost cap.
#
# SYNC: This resolves the clamp result through the SAME provider config the runner
# uses, a byte-faithful port of providers/claude.sh loki_apply_max_tier_clamp plus
# the PROVIDER_MODEL_FAST / PROVIDER_MODEL_DEVELOPMENT resolution chains
# (claude.sh:55-67, :318). The same port also lives in the dashboard
# (dashboard/server.py _provider_model_fast / _provider_model_development /
# _clamp_to_max_tier). All three readers MUST agree; the agreement is locked by
# the parity test in tests/test-model-override.sh (resolver parity matrix) and the
# cross-route tests in test-plan-command.sh. If you change resolution here, change
# it in claude.sh AND dashboard/server.py, and re-run those tests. The 'or' chains
# mirror bash ':-' empty-string-fallthrough; allow_haiku uses an exact 'true'
# match to mirror bash [ \"\$x\" = \"true\" ]. v7.104.0: PROVIDER_MODEL_DEVELOPMENT
# and PROVIDER_MODEL_PLANNING default to SONNET (was opus); PROVIDER_MODEL_FAST is
# sonnet by default (haiku when ALLOW_HAIKU).
_allow_haiku = (os.environ.get('LOKI_ALLOW_HAIKU', 'false') or 'false') == 'true'
def _provider_model_fast():
    return (os.environ.get('LOKI_CLAUDE_MODEL_FAST')
            or os.environ.get('LOKI_MODEL_FAST')
            or ('haiku' if _allow_haiku else 'sonnet'))
def _provider_model_development():
    # v7.104.0: CLAUDE_DEFAULT_DEVELOPMENT=sonnet (was opus). ALLOW_HAIKU no longer
    # changes dev (already sonnet); it only lowers fast to haiku.
    return (os.environ.get('LOKI_CLAUDE_MODEL_DEVELOPMENT')
            or os.environ.get('LOKI_MODEL_DEVELOPMENT')
            or 'sonnet')
def _provider_model_planning():
    # claude.sh:55 -> LOKI_CLAUDE_MODEL_PLANNING > LOKI_MODEL_PLANNING > sonnet.
    # v7.104.0: CLAUDE_DEFAULT_PLANNING=sonnet (was opus).
    return (os.environ.get('LOKI_CLAUDE_MODEL_PLANNING')
            or os.environ.get('LOKI_MODEL_PLANNING')
            or 'sonnet')
_max_tier = (os.environ.get('LOKI_MAX_TIER', '') or '').strip().lower()
def _loki_clamp_alias(alias):
    if not _max_tier:
        return alias
    if _max_tier == 'haiku':
        return _provider_model_fast()
    if _max_tier == 'sonnet':
        # alias passed as both model and tier (override-path convention): the
        # runner's sonnet arm reduces to 'downgrade iff alias==fable'.
        return _provider_model_development() if alias == 'fable' else alias
    if _max_tier == 'opus':
        return 'opus' if alias == 'fable' else alias
    return alias

# Session-pin tier route (the runner's NO-OVERRIDE path). When there is no
# mid-flight override file, the runner does NOT feed the session alias straight
# to --model. It maps the alias to an abstract TIER (run.sh:12331 -- opus->
# planning, sonnet->development, haiku->fast, fable->fable) and resolves that
# tier through resolve_model_for_tier (claude.sh:353), which reads
# PROVIDER_MODEL_PLANNING/DEVELOPMENT/FAST and then applies
# loki_apply_max_tier_clamp(model, REAL_tier). v7.104.0: with the sonnet default,
# a 'sonnet' SESSION pin now dispatches sonnet (development tier ->
# PROVIDER_MODEL_DEVELOPMENT=sonnet), matching a 'sonnet' OVERRIDE file. The
# estimator must quote the model the runner actually dispatches; on stock config
# the session-pin path and override path now agree on sonnet for dev/planning
# tiers (they diverge only when the tier defaults are overridden by env).
#
# SYNC: byte-faithful with run.sh's session-pin case + claude.sh
# resolve_model_for_tier + loki_apply_max_tier_clamp. The same resolver lives in
# the dashboard (dashboard/server.py _resolve_session_pin). Locked by the
# session-pin parity matrix and the no-cap stock cross-route cases in
# tests/test-model-override.sh.
def _resolve_session_pin(alias):
    # v7.104.0 opus-pin fix: mirror run.sh. Post the Sonnet-5 default flip, NO
    # tier resolves to opus, so an opus SESSION pin must dispatch opus directly
    # (not route through planning->sonnet). Clamped by LOKI_MAX_TIER so the
    # ceiling still binds. sonnet/haiku pins stay on the tier route (preserving
    # the ALLOW_HAIKU gate for haiku).
    if alias == 'opus':
        if not _max_tier:
            return 'opus'
        if _max_tier == 'haiku':
            return _provider_model_fast()
        # sonnet cap: clamp opus down to the development tier model (sonnet stock).
        if _max_tier == 'sonnet':
            return _provider_model_development()
        return 'opus'  # opus cap (or any higher): opus stands.
    # alias -> abstract tier (run.sh:12331). Unknown -> development (the runner's
    # '*' arm passes CURRENT_TIER through to resolve_model_for_tier, whose '*'
    # default is PROVIDER_MODEL_DEVELOPMENT, i.e. the development tier).
    _pin_tier = {
        'sonnet': 'development',
        'haiku': 'fast',
        'fable': 'fable',
        # Raw tier-name pins (run.sh:12336 passthrough arm) map to their own
        # tier, NOT through the alias table. So pin=fast -> fast tier ->
        # PROVIDER_MODEL_FAST (sonnet stock / haiku ALLOW_HAIKU), matching the
        # runner's dispatch instead of collapsing onto development.
        'planning': 'planning',
        'development': 'development',
        'fast': 'fast',
    }.get(alias, 'development')
    # tier -> model (claude.sh resolve_model_for_tier explicit arms).
    if _pin_tier == 'planning':
        _m = _provider_model_planning()
    elif _pin_tier == 'fast':
        _m = _provider_model_fast()
    elif _pin_tier == 'fable':
        # fable unavailable, collapse to opus. Claude Fable 5 is not available at
        # the Claude API (advises use Opus 4.8); the runner dispatches opus for a
        # fable pin (claude.sh resolve_model_for_tier, run.sh dispatch backstop).
        # Quote opus pricing (\$4/\$20) so the estimator agrees with dispatch and
        # the dashboard (v7.39.1).
        _m = 'opus'
    else:  # development (and the '*' fallthrough)
        _m = _provider_model_development()
    # Explicit top / floor setup, mirrors claude.sh resolve_model_for_tier: a
    # planning pin dispatches opus, a fast pin haiku (skipped when
    # LOKI_ALLOW_HAIKU is explicitly set to anything but 'true'), unless legacy
    # tier switching or an explicit per-tier pin applies.
    if os.environ.get('LOKI_LEGACY_TIER_SWITCHING') != 'true':
        if (_pin_tier == 'planning' and alias == 'planning'
                and not (os.environ.get('LOKI_CLAUDE_MODEL_PLANNING') or os.environ.get('LOKI_MODEL_PLANNING'))):
            _m = 'opus'
        elif (_pin_tier == 'fast' and alias == 'fast'
                and not (os.environ.get('LOKI_CLAUDE_MODEL_FAST') or os.environ.get('LOKI_MODEL_FAST'))
                and os.environ.get('LOKI_ALLOW_HAIKU', 'true') == 'true'):
            _m = 'haiku'
    # Apply the shared LOKI_MAX_TIER clamp with the REAL tier and resolved model
    # (loki_apply_max_tier_clamp(model, tier)), NOT the alias==alias override
    # convention. This is the one place where the two paths legitimately diverge
    # (e.g. opus pin + sonnet cap + ALLOW_HAIKU: tier route -> sonnet, because the
    # planning tier downgrades to PROVIDER_MODEL_DEVELOPMENT=sonnet).
    if not _max_tier:
        return _m
    if _max_tier == 'haiku':
        return _provider_model_fast()
    if _max_tier == 'sonnet':
        if _pin_tier in ('planning', 'fable') or _m == 'fable':
            return _provider_model_development()
        return _m
    if _max_tier == 'opus':
        return 'opus' if _m == 'fable' else _m
    return _m
# Resolve the model the runner will ACTUALLY dispatch for the pinned session,
# choosing the correct route:
#   - OVERRIDE file present: the runner feeds the alias straight to --model via
#     loki_apply_max_tier_clamp(alias, alias). Use _loki_clamp_alias (the
#     override-path clamp). A 'sonnet' override dispatches sonnet.
#   - No override (session pin): the runner maps alias -> tier ->
#     resolve_model_for_tier -> clamp(model, real_tier). Use _resolve_session_pin.
#     A 'sonnet' session pin dispatches SONNET (development tier; CLAUDE_DEFAULT_DEVELOPMENT=sonnet since v7.104.0).
# This is the task-568 fix: price the actually-dispatched model on BOTH routes.
_is_override = (_session_model_source == '.loki/state/model-override')
if _is_override:
    _dispatched_model = _loki_clamp_alias(session_model_env)
else:
    _dispatched_model = _resolve_session_pin(session_model_env)

# fable unavailable, collapse to opus (final dispatch backstop, mirrors run.sh).
# The override-path clamp (_loki_clamp_alias) leaves an uncapped fable override as
# 'fable', but the runner's dispatch backstop collapses tier_param=='fable' to
# opus before --model. Apply the same collapse here so the OVERRIDE route quotes
# opus too, agreeing with dispatch and the dashboard. The session-pin route is
# already collapsed inside _resolve_session_pin; this also covers a fable override
# file (v7.39.1).
if _dispatched_model == 'fable':
    _dispatched_model = 'opus'

# Keep session_model_env (the alias) for token-volume tier mapping and provenance,
# but record when the cost ceiling actually changed the dispatched model so the
# provenance line is honest about the clamp.
if _max_tier and _dispatched_model != session_model_env:
    _session_model_source = 'LOKI_MAX_TIER (clamped)'
# Clear the architect-fable disclosure when the ceiling would clamp the architect
# iteration too. This is INDEPENDENT of whether the session model itself changed:
# e.g. an opus pin under a sonnet cap leaves the session model opus (no change),
# but the iter-0 fable architect tier still clamps down to development. Nesting
# this under the session-changed branch would over-quote a Fable iteration the
# runner actually dispatches as opus (estimator-vs-runner divergence).
if _max_tier and _fable_architect and _loki_clamp_alias('fable') != 'fable':
    _fable_architect = False

# v7.29.0: Honor LOKI_COMPLEXITY -- the SAME env var the runner honors at
# run.sh:920 -- so a forced-tier run (e.g. 'loki demo' / 'loki start --simple'
# which export LOKI_COMPLEXITY=simple) quotes the tier it will actually run,
# not the content-derived tier. Without this the estimate diverges from
# execution (audit finding #9). Runner vocabulary is auto|simple|standard|complex;
# the estimator's internal tiers are simple|moderate|complex|enterprise, so
# 'standard' aliases to 'moderate' (the 6-12 iteration analog). 'auto', empty,
# and any unrecognized value are ignored (content tier is used) -- honest by
# construction. The override is applied AFTER the content score-to-tier map.
_forced_complexity_raw = (os.environ.get('LOKI_COMPLEXITY', '') or '').strip().lower()
forced_complexity = {'standard': 'moderate'}.get(_forced_complexity_raw, _forced_complexity_raw)
if forced_complexity not in ('simple', 'moderate', 'complex', 'enterprise'):
    forced_complexity = ''

# Map session model name to tier key used in tokens_per_tier below. This keys the
# token VOLUME per iteration off the work tier the session pin selects (e.g. a
# sonnet pin = the development tier = 80k/15k tokens). It does NOT decide the
# priced model: that is _dispatched_model, the model the runner actually invokes.
# Unknown models fall through to 'development' as a safe default.
_session_tier_map = {
    'fable': 'advisor',
    'opus': 'planning',
    'sonnet': 'development',
    'haiku': 'fast',
    # Raw tier-name pins key off their own work tier directly (they ARE tier
    # names), mirroring run.sh's session-pin passthrough arm.
    'planning': 'planning',
    'development': 'development',
    'fast': 'fast',
}
session_tier = _session_tier_map.get(session_model_env, 'development')

# Pricing-table key (Fable/Opus/Sonnet/Haiku) for the model the runner dispatches.
# The loop below uses tokens_per_tier[tier] for token VOLUME but prices by this
# model, so the quote reflects the actually-dispatched model on every route.
_PRICED_MODEL_KEY = {'fable': 'Fable', 'opus': 'Opus', 'sonnet': 'Sonnet', 'haiku': 'Haiku'}
def _priced_model_for(alias):
    return _PRICED_MODEL_KEY.get((alias or '').strip().lower(), 'Opus')

# Colors (disabled for JSON mode)
if show_json:
    RED = GREEN = YELLOW = BLUE = CYAN = BOLD = DIM = NC = ''
else:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    BOLD = '\033[1m'
    DIM = '\033[2m'
    NC = '\033[0m'

# --- Read and analyze PRD ---
with open(prd_path, 'r') as f:
    content = f.read()

prd_words = len(content.split())
prd_lines = content.count('\n') + 1

# Detect PRD format
is_json_prd = prd_path.endswith('.json')
sections = []
features = []
endpoints = []
integrations = []
databases = []
ui_components = []

if is_json_prd:
    try:
        prd_data = json.loads(content)
        features = prd_data.get('features', prd_data.get('requirements', []))
        if isinstance(features, list):
            features = [f.get('title', f.get('name', str(f))) if isinstance(f, dict) else str(f) for f in features]
        else:
            features = []
    except Exception:
        features = []
else:
    # Markdown analysis
    for line in content.split('\n'):
        stripped = line.strip()
        if stripped.startswith('## '):
            sections.append(stripped[3:].strip())
        elif stripped.startswith('### '):
            sections.append(stripped[4:].strip())

    # Count features: headers + checkboxes
    feature_patterns = re.findall(r'^(?:##?\s+|[-*]\s+\[.\]\s+)(.+)', content, re.MULTILINE)
    features = feature_patterns

    # Count API endpoints
    endpoint_patterns = re.findall(
        r'(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+[/\w{}:.-]+',
        content, re.IGNORECASE
    )
    endpoints = endpoint_patterns

    # Count external integrations
    integration_keywords = [
        'oauth', 'saml', 'oidc', 'stripe', 'twilio', 'sendgrid',
        'aws', 'gcp', 'azure', 'firebase', 's3', 'redis', 'kafka',
        'elasticsearch', 'rabbitmq', 'webhook', 'socket.io', 'websocket',
        'graphql', 'grpc', 'docker', 'kubernetes', 'k8s'
    ]
    content_lower = content.lower()
    for kw in integration_keywords:
        if kw in content_lower:
            integrations.append(kw)

    # Count database mentions
    # Count DISTINCT data stores. This feeds a +1-complexity-for-multiple
    # rule, so what counts as multiple is load-bearing: it pushed a
    # 322-word issue from moderate to complex, and complex means 18 iterations
    # instead of 9.
    #
    # Three ways the old flat list over-counted a single-database project:
    #   1. Aliases. 'postgresql' and 'postgres' were separate entries, so one
    #      PostgreSQL app scored TWO data stores by itself. That alone tripped
    #      the rule on a real user's issue.
    #   2. ORMs. prisma/typeorm/sequelize/drizzle/knex are ACCESS LAYERS, not
    #      stores. Prisma plus PostgreSQL is one database, not two.
    #   3. Generic words. 'database' and 'migration' matched any PRD that used
    #      the words at all, so a migration-to-the-database line scored two.
    #
    # Aliases collapse to one canonical name, ORMs and generic terms are gone.
    db_aliases = {
        'postgresql': 'postgres', 'postgres': 'postgres', 'psql': 'postgres',
        'mysql': 'mysql', 'mariadb': 'mysql',
        'mongodb': 'mongodb', 'mongo': 'mongodb',
        'sqlite': 'sqlite',
        'dynamodb': 'dynamodb',
        'cassandra': 'cassandra',
        'redis': 'redis',
        'elasticsearch': 'elasticsearch',
        'clickhouse': 'clickhouse',
    }
    _seen_db = set()
    for kw, canonical in db_aliases.items():
        if kw in content_lower and canonical not in _seen_db:
            _seen_db.add(canonical)
            databases.append(canonical)

    # Count UI components
    ui_keywords = ['dashboard', 'form', 'table', 'modal', 'navbar',
                   'sidebar', 'chart', 'graph', 'calendar', 'kanban',
                   'drag.and.drop', 'responsive', 'mobile', 'animation']
    for kw in ui_keywords:
        if re.search(kw, content_lower):
            ui_components.append(kw)

feature_count = len(features)
endpoint_count = len(endpoints)
integration_count = len(integrations)
db_count = len(databases)
ui_count = len(ui_components)
section_count = len(sections)

# --- Determine complexity ---
complexity_score = 0
complexity_reasons = []

if prd_words < 200 and feature_count < 5:
    complexity = 'simple'
    complexity_reasons.append('Short PRD (<200 words, <5 features)')
elif prd_words > 3000 or feature_count > 30:
    complexity_score += 3
    complexity_reasons.append(f'Large PRD ({prd_words} words, {feature_count} features)')
elif prd_words > 1000 or feature_count > 15:
    complexity_score += 2
    complexity_reasons.append(f'Medium PRD ({prd_words} words, {feature_count} features)')
else:
    complexity_score += 1
    complexity_reasons.append(f'Standard PRD ({prd_words} words, {feature_count} features)')

if endpoint_count > 20:
    complexity_score += 2
    complexity_reasons.append(f'{endpoint_count} API endpoints detected')
elif endpoint_count > 5:
    complexity_score += 1
    complexity_reasons.append(f'{endpoint_count} API endpoints detected')

if integration_count > 3:
    complexity_score += 2
    complexity_reasons.append(str(integration_count) + ' external integrations: ' + ', '.join(integrations[:5]))
elif integration_count > 0:
    complexity_score += 1
    complexity_reasons.append(str(integration_count) + ' external integrations: ' + ', '.join(integrations))

if db_count > 1:
    complexity_score += 1
    complexity_reasons.append('Multiple data stores: ' + ', '.join(databases[:4]))

if ui_count > 5:
    complexity_score += 1
    complexity_reasons.append(f'{ui_count} UI components detected')

# Map score to tier
if complexity_score <= 1:
    complexity = 'simple'
elif complexity_score <= 3:
    complexity = 'moderate'
elif complexity_score <= 5:
    complexity = 'complex'
else:
    complexity = 'enterprise'

# v7.29.0: apply the forced-tier override AFTER the content score-to-tier map,
# so it is not clobbered by the assignment above. Everything downstream
# (iterations, tokens, cost, time) derives from the complexity variable, so the
# whole estimate becomes consistent with what the run will actually do.
if forced_complexity:
    complexity = forced_complexity
    complexity_reasons.insert(0, 'Complexity forced via LOKI_COMPLEXITY=' + _forced_complexity_raw)

# --- Estimate iterations ---
iteration_map = {
    'simple': (3, 5),
    'moderate': (6, 12),
    'complex': (12, 24),
    'enterprise': (24, 48),
}
min_iter, max_iter = iteration_map[complexity]

# --- Capability-aware iteration budget ---------------------------------------
# The map above was written when a model could not hold a whole feature in one
# pass, so work was chopped into many small iterations. On a frontier tier that
# is now actively harmful: each iteration boundary is a context reset, and
# re-planning mid-feature is where a run loses the thread and burns money.
#
# A frontier model should be handed the whole job and left to finish it. So the
# budget is divided by the capability tier the run will actually dispatch,
# rather than assuming the weakest case for everyone. The floor is 1, never 0.
#
# This scales the ESTIMATE and the ceiling, not the work. A run that genuinely
# needs more iterations still takes them; nothing here forces an early stop.
_tier_raw = (os.environ.get('LOKI_SESSION_MODEL') or '').strip().lower()
_frontier = ('high', 'planning', 'opus', 'sol')
_small = ('small', 'fast', 'haiku', 'luna')
if _tier_raw in _frontier:
    # Frontier: one pass for anything short of enterprise scope.
    min_iter = 1
    max_iter = max(1, max_iter // 4)
elif _tier_raw in _small:
    # Small models genuinely benefit from more, smaller steps.
    pass
else:
    # medium / unset: the common default. Halve the legacy budget -- current
    # workhorse models complete in far fewer passes than this map assumed.
    min_iter = max(1, min_iter // 2)
    max_iter = max(min_iter, max_iter // 2)

# Adjust based on specific counts
if endpoint_count > 10:
    max_iter = max(max_iter, max_iter + endpoint_count // 5)
if integration_count > 2:
    max_iter += integration_count

estimated_iterations = (min_iter + max_iter) // 2

# --- Token estimation per iteration by RARV tier ---
# RARV cycle: 4 iterations per cycle (plan, develop, develop, test)
# Tokens estimated per iteration step
tokens_per_tier = {
    'advisor': {'input': 50000, 'output': 8000, 'model': 'Fable'},
    'planning': {'input': 50000, 'output': 8000, 'model': 'Opus'},
    'development': {'input': 80000, 'output': 15000, 'model': 'Sonnet'},
    'fast': {'input': 30000, 'output': 5000, 'model': 'Haiku'},
}

# Pricing per 1M tokens. Fable 5 is the top-tier advisory model at 2.5x
# Opus per token (published \$10/\$50 in/out vs Opus 5.5 \$4/\$20). The Opus row was
# corrected to the published Opus 5.5 \$4/\$20 (platform.claude.com pricing, 2026-10-08), and the Haiku
# row from a stale \$0.25/\$1.25 (Haiku 3.5) to Haiku 5.5 \$0.10/\$0.50 (the haiku alias target
# in the model catalog, up to 100K prompt tokens), so all four rows now
# match the three canonical model-keyed tables (run.sh pricing.json, run.sh
# check_budget_limit, dashboard _DEFAULT_PRICING). The Fable row (\$10/\$50) is
# kept as the reference price, but since v7.39.1 fable is unavailable at the API
# and a fable pin RESOLVES to Opus everywhere -- so a fable-pinned run both
# dispatches AND quotes Opus (\$4/\$20), matching the runner. The Fable price row
# is only reachable as an explicit advisory reference, not via a session pin.
pricing = {
    'Fable':  {'input': 10.00, 'output': 50.00},
    'Opus':   {'input': 4.00, 'output': 20.00},
    'Sonnet': {'input': 2.00, 'output': 10.00},
    # Haiku = Haiku 5.5 up to 100K prompt tokens (alias target in the model catalog)
    'Haiku':  {'input': 0.10, 'output': 0.50},
}

# Build per-iteration plan
iteration_plan = []
total_input_tokens = 0
total_output_tokens = 0
total_cost = 0.0
tier_totals = {'Fable': 0.0, 'Opus': 0.0, 'Sonnet': 0.0, 'Haiku': 0.0}
tier_iterations = {'Fable': 0, 'Opus': 0, 'Sonnet': 0, 'Haiku': 0}

for i in range(estimated_iterations):
    rarv_step = i % 4
    if rarv_step == 0:
        phase_label = 'Reason (Planning)'
    elif rarv_step == 1:
        phase_label = 'Act (Implementation)'
    elif rarv_step == 2:
        phase_label = 'Reflect (Review)'
    else:
        phase_label = 'Verify (Testing)'

    if legacy_tier_switching:
        # Legacy pre-v6.81.0 behavior: rotate tier per RARV step.
        if rarv_step == 0:
            tier = 'planning'
        elif rarv_step in (1, 2):
            tier = 'development'
        else:
            tier = 'fast'
    else:
        # v6.81.0+: main loop is pinned to LOKI_SESSION_MODEL. Estimate
        # MUST use the same tier for all iterations to avoid quoting a
        # cost higher than the user will actually pay.
        tier = session_tier
        # Architect opt-in: the runner routes ONLY the first iteration to fable
        # (the architecture pass). Mirror that here so the quote is not blind to
        # the lever: iteration 0 -> advisor (fable), the rest -> session tier.
        if _fable_architect and i == 0:
            tier = 'advisor'

    info = tokens_per_tier[tier]
    # Token VOLUME comes from the work tier (info), but the priced MODEL is the
    # model the runner actually dispatches. In legacy rotation the per-tier model
    # IS the dispatched model. In session-pinned mode (the default) the runner
    # resolves the pin through provider config (_dispatched_model): a sonnet pin
    # dispatches opus, so we price Opus, not the tier's static 'Sonnet' label.
    # The fable-architect iteration 0 collapses to opus at dispatch (Fable 5 is
    # unavailable at the claude API), so it is priced as Opus, not Fable (v7.39.1).
    if legacy_tier_switching:
        model = info['model']
    elif _fable_architect and i == 0:
        # Architect opt-in iter-0: fable unavailable, the runner dispatches opus
        # for the architecture pass (LOKI_FABLE_ARCHITECT collapses to opus in
        # run.sh / claude.sh). Token VOLUME stays the advisor/planning tier
        # (50k/8k), but the priced model is Opus, not Fable (v7.39.1).
        model = 'Opus'
    else:
        model = _priced_model_for(_dispatched_model)
    inp = info['input']
    out = info['output']

    cost = (inp / 1_000_000) * pricing[model]['input'] + \
           (out / 1_000_000) * pricing[model]['output']

    total_input_tokens += inp
    total_output_tokens += out
    total_cost += cost
    tier_totals[model] += cost
    tier_iterations[model] += 1

    iteration_plan.append({
        'iteration': i + 1,
        'phase': phase_label,
        'model': model,
        'input_tokens': inp,
        'output_tokens': out,
        'cost_usd': round(cost, 4),
    })

# --- Execution plan (what each cycle tackles) ---
execution_phases = []
cycle_count = math.ceil(estimated_iterations / 4)
phase_names = []

# Generate phase names based on PRD content
if sections:
    phase_names = sections[:cycle_count]
else:
    # Generic phases based on complexity
    generic = [
        'Project setup and scaffolding',
        'Core data models and database schema',
        'API endpoints and business logic',
        'Frontend components and pages',
        'Authentication and authorization',
        'External integrations',
        'Testing and quality assurance',
        'Performance optimization',
        'Documentation and deployment',
        'Edge cases and error handling',
        'Security hardening',
        'Final polish and review',
    ]
    phase_names = generic[:cycle_count]

for idx, name in enumerate(phase_names):
    execution_phases.append({
        'cycle': idx + 1,
        'iterations': f'{idx * 4 + 1}-{min((idx + 1) * 4, estimated_iterations)}',
        'focus': name,
    })

# --- Time estimation ---
# Average ~3.5 minutes per iteration (API calls + processing)
minutes_per_iteration = 3.5
total_minutes = estimated_iterations * minutes_per_iteration
if total_minutes < 60:
    time_estimate = f'{int(total_minutes)} minutes'
elif total_minutes < 120:
    time_estimate = f'{int(total_minutes // 60)} hour {int(total_minutes % 60)} minutes'
else:
    time_estimate = f'{total_minutes / 60:.1f} hours'

min_time = min_iter * minutes_per_iteration
max_time = max_iter * minutes_per_iteration

# --- Quality gates ---
quality_gates = [
    {'gate': 'Static Analysis', 'trigger': 'Every iteration', 'applies': True},
    {'gate': 'Unit Tests', 'trigger': 'Verify phase (every 4th iteration)', 'applies': True},
    {'gate': 'Code Review (3-reviewer)', 'trigger': 'After implementation cycles', 'applies': complexity in ('complex', 'enterprise')},
    {'gate': 'Anti-sycophancy Check', 'trigger': 'On unanimous review approval', 'applies': complexity in ('complex', 'enterprise')},
    {'gate': 'Integration Tests', 'trigger': 'Mid-project and final', 'applies': integration_count > 0 or endpoint_count > 3},
    {'gate': 'Coverage Gate (>80%)', 'trigger': 'Final verification', 'applies': True},
    {'gate': 'Security Scan', 'trigger': 'Before completion', 'applies': integration_count > 0},
    {'gate': 'Performance Benchmark', 'trigger': 'Before completion', 'applies': complexity in ('complex', 'enterprise')},
    {'gate': 'Completion Council', 'trigger': 'After all phases', 'applies': True},
]
active_gates = [g for g in quality_gates if g['applies']]

# --- Provider recommendation ---
if integration_count > 3 or complexity in ('complex', 'enterprise'):
    recommended_provider = 'Claude'
    provider_reason = 'Full feature support needed (subagents, parallel, MCP)'
elif complexity == 'simple' and endpoint_count == 0:
    recommended_provider = 'Any (Claude/Codex/Cline)'
    provider_reason = 'Simple project works with all providers'
else:
    recommended_provider = 'Claude'
    provider_reason = 'Best iteration quality and tool support'

# --- Output ---
if show_json:
    result = {
        'prd_file': prd_path,
        'prd_stats': {
            'words': prd_words,
            'lines': prd_lines,
            'sections': section_count,
            'features': feature_count,
            'endpoints': endpoint_count,
            'integrations': integrations,
            'databases': databases,
            'ui_components': ui_count,
        },
        'complexity': {
            'tier': complexity,
            'score': complexity_score,
            'reasons': complexity_reasons,
        },
        'iterations': {
            'estimated': estimated_iterations,
            'range': [min_iter, max_iter],
            'rarv_cycles': cycle_count,
        },
        'tokens': {
            'total_input': total_input_tokens,
            'total_output': total_output_tokens,
            'total': total_input_tokens + total_output_tokens,
        },
        'cost': {
            'total_usd': round(total_cost, 2),
            'by_model': {k: round(v, 2) for k, v in tier_totals.items()},
            'iterations_by_model': tier_iterations,
        },
        'time': {
            'estimated': time_estimate,
            'minutes': round(total_minutes, 1),
            'range_minutes': [round(min_time, 1), round(max_time, 1)],
        },
        'execution_plan': execution_phases,
        'quality_gates': [g['gate'] for g in active_gates],
        'provider': {
            'recommended': recommended_provider,
            'reason': provider_reason,
        },
        'moat': {'verified_completion': True, 'cost_honest': True},
    }
    if show_verbose:
        result['iteration_details'] = iteration_plan
    print(json.dumps(result, indent=2))
    sys.exit(0)

# --- Formatted output ---
print()
print(f'{BOLD}PRD Analysis: {os.path.basename(prd_path)}{NC}')
print(f'{DIM}' + '=' * 60 + f'{NC}')

# PRD Stats
print(f'\n{CYAN}PRD Statistics{NC}')
print(f'  Words: {prd_words}  |  Lines: {prd_lines}  |  Sections: {section_count}')
print(f'  Features: {feature_count}  |  Endpoints: {endpoint_count}  |  Integrations: {integration_count}')
if databases:
    print('  Data stores: ' + ', '.join(databases[:4]))
if ui_count > 0:
    print(f'  UI components: {ui_count}')

# Complexity
color = {'simple': GREEN, 'moderate': YELLOW, 'complex': RED, 'enterprise': RED}
cx_color = color.get(complexity, NC)
print(f'\n{CYAN}Complexity{NC}')
if forced_complexity:
    print(f'  Tier: {cx_color}{BOLD}{complexity.upper()}{NC} (forced via LOKI_COMPLEXITY={_forced_complexity_raw})')
else:
    print(f'  Tier: {cx_color}{BOLD}{complexity.upper()}{NC} (score: {complexity_score})')
for reason in complexity_reasons:
    print(f'  {DIM}- {reason}{NC}')

# Iterations
print(f'\n{CYAN}Estimated Iterations{NC}')
print(f'  Count: {BOLD}{estimated_iterations}{NC} (range: {min_iter}-{max_iter})')
print(f'  RARV cycles: {cycle_count} (4 iterations per cycle)')
fable_n = tier_iterations.get('Fable', 0)
opus_n = tier_iterations.get('Opus', 0)
sonnet_n = tier_iterations.get('Sonnet', 0)
haiku_n = tier_iterations.get('Haiku', 0)
if legacy_tier_switching:
    print(f'  Model distribution: Opus x{opus_n} | Sonnet x{sonnet_n} | Haiku x{haiku_n}')
    print(f'  {DIM}(legacy rotation: LOKI_LEGACY_TIER_SWITCHING=true){NC}')
else:
    # Session-pinned: exactly one tier has all iterations.
    pinned_parts = []
    if fable_n > 0:
        pinned_parts.append(f'Fable x{fable_n}')
    if opus_n > 0:
        pinned_parts.append(f'Opus x{opus_n}')
    if sonnet_n > 0:
        pinned_parts.append(f'Sonnet x{sonnet_n}')
    if haiku_n > 0:
        pinned_parts.append(f'Haiku x{haiku_n}')
    print('  Model distribution: ' + (' | '.join(pinned_parts) if pinned_parts else '(none)'))
    # Provenance: name the lever that actually selected the pinned model, never a
    # false attribution. _session_model_source is the real source (env, override
    # file, or maxTier clamp) tracked at selection time. When the session pin is a
    # tier alias that resolves to a DIFFERENT dispatched model (the default: a
    # 'sonnet' pin -> development tier -> opus), show the resolution so the
    # distribution above (Opus x4) is legible against the pin name (sonnet).
    if (not _is_override) and _dispatched_model != session_model_env:
        print(f'  {DIM}(pinned via {_session_model_source}={session_model_env} -> {session_tier} tier -> {_dispatched_model}; set LOKI_LEGACY_TIER_SWITCHING=true to rotate){NC}')
    else:
        print(f'  {DIM}(pinned via {_session_model_source}={session_model_env}; set LOKI_LEGACY_TIER_SWITCHING=true to rotate){NC}')
    if _fable_architect:
        # fable unavailable: the architect opt-in now runs the architecture pass
        # on opus (Claude Fable 5 is not available at the Claude API). Disclose
        # Opus so the quote matches dispatch (v7.39.1).
        print(f'  {DIM}(+ 1 architecture iteration on Opus via LOKI_FABLE_ARCHITECT=1){NC}')
    if fable_n > 0:
        print(f'  {YELLOW}Fable 5 is the top-tier advisory model priced at 2x Opus ({chr(36)}10/{chr(36)}50 per MTok).{NC}')

# Tokens
total_tok = total_input_tokens + total_output_tokens
print(f'\n{CYAN}Token Usage Estimate{NC}')
print(f'  Input:  {total_input_tokens:>12,} tokens')
print(f'  Output: {total_output_tokens:>12,} tokens')
print(f'  Total:  {total_tok:>12,} tokens')

# Cost
ds = chr(36)  # dollar sign
print(f'\n{CYAN}Cost Estimate{NC}')
print(f'  {BOLD}Total: {ds}{total_cost:.2f}{NC}')
for model in ['Fable', 'Opus', 'Sonnet', 'Haiku']:
    # v6.81.1: suppress zero-cost tiers (e.g. session-pinned runs). Keeps
    # the breakdown focused on models actually used. Legacy mode still
    # shows all three because each tier has non-zero iterations.
    if tier_iterations.get(model, 0) == 0 and tier_totals.get(model, 0) == 0:
        continue
    pct = (tier_totals[model] / total_cost * 100) if total_cost > 0 else 0
    bar_len = int(pct / 5)
    bar = '#' * bar_len + '.' * (20 - bar_len)
    mc = tier_totals[model]
    print('  {:>6}: {}{:>7.2f} ({:4.1f}%) [{}]'.format(model, ds, mc, pct, bar))

# Time
print(f'\n{CYAN}Time Estimate{NC}')
print(f'  Estimated: {BOLD}{time_estimate}{NC}')
min_t_str = f'{int(min_time)} min' if min_time < 60 else f'{min_time/60:.1f} hr'
max_t_str = f'{int(max_time)} min' if max_time < 60 else f'{max_time/60:.1f} hr'
print(f'  Range: {min_t_str} - {max_t_str}')

# Execution plan
print(f'\n{CYAN}Execution Plan{NC}')
for phase in execution_phases:
    c_num = phase.get('cycle', '')
    c_iters = phase.get('iterations', '')
    c_focus = phase.get('focus', '')
    print(f'  {BOLD}Cycle {c_num}{NC} (iterations {c_iters}): {c_focus}')

# Quality gates
print(f'\n{CYAN}Quality Gates ({len(active_gates)} active){NC}')
for g in active_gates:
    g_name = g.get('gate', '')
    g_trig = g.get('trigger', '')
    print(f'  {GREEN}[*]{NC} {g_name} -- {DIM}{g_trig}{NC}')

# Provider recommendation
print(f'\n{CYAN}Recommended Provider{NC}')
print(f'  {BOLD}{recommended_provider}{NC}')
print(f'  {DIM}{provider_reason}{NC}')

# Verbose: per-iteration breakdown
if show_verbose:
    print(f'\n{CYAN}Per-Iteration Breakdown{NC}')
    hdr = '  ' + DIM + '{:>4}  {:<24} {:<7} {:>8} {:>8} {:>8}'.format('Iter', 'Phase', 'Model', 'Input', 'Output', 'Cost') + NC
    print(hdr)
    print('  ' + DIM + '-' * 65 + NC)
    for it in iteration_plan:
        i_num = it.get('iteration', 0)
        i_phase = it.get('phase', '')
        i_model = it.get('model', '')
        i_inp = it.get('input_tokens', 0)
        i_out = it.get('output_tokens', 0)
        i_cost = it.get('cost_usd', 0)
        print('  {:>4}  {:<24} {:<7} {:>7,} {:>7,} {}{:>6.2f}'.format(i_num, i_phase, i_model, i_inp, i_out, ds, i_cost))

print(f'\n{DIM}This is an estimate. Actual usage depends on PRD complexity,')
print(f'code review cycles, and test failures.{NC}')

print(f'\n{CYAN}Why this estimate is trustworthy{NC}')
print(f'{DIM}Verified completion. The completion council refuses to call work done')
print(f'without evidence - a real diff and green tests, or it blocks.{NC}')
print(f'{DIM}Cost honesty. The model quoted here is the model the run dispatches')
print(f'and the dashboard reports. Quote, dashboard, and dispatch agree.{NC}')
print()
" "$prd_path" "$show_json" "$show_verbose"
}

# Dry-run PRD analysis and cost estimation (v6.18.0; refactored v6.81.1)
# Thin wrapper around show_prd_plan() so the same analysis is reusable from
# `loki start`, `loki run`, and other commands without duplicated code.
# Read-only view of .loki/state/requirements-map.json (REQTRACE; contract: docs/spec-first-intent.md).
_plan_show_requirements() {
    local as_json="${1:-false}" map="${LOKI_DIR:-.loki}/state/requirements-map.json"
    if [ ! -s "$map" ]; then
        if [ "$as_json" = true ]; then printf '{"error":"no requirements map","path":"%s"}\n' "$map"; return 1; fi
        echo "No requirements map at $map (written after a run that had a PRD checklist, a spec, or a multi-requirement PRD)."
        return 1
    fi
    if [ "$as_json" = true ]; then cat "$map"; return 0; fi
    LOKI_RM="$map" python3 -I -c "
import json, os, sys
try:
    m = json.load(open(os.environ['LOKI_RM']))
    reqs = m['requirements']
except Exception:
    print('requirements map is unreadable: ' + os.environ['LOKI_RM']); sys.exit(1)
cov = sum(1 for r in reqs if r.get('status') == 'covered')
print('Requirements: %d/%d covered (revision %s)' % (cov, len(reqs), str(m.get('revision', ''))[:12]))
for r in reqs:
    chk = ('  <- ' + ', '.join(r.get('checks', []))) if r.get('checks') else ''
    print('  %s %-9s %-9s %s%s' % (r.get('id'), r.get('status'), r.get('method'), str(r.get('text', ''))[:80], chk))
"
}

# loki try: clone a public repo into a run-owned temp dir, pick a starter task, run it
# there, keep only the receipt. The caller's checkout is never touched. The temp helpers
# below follow the loki_run_tmp_create/cleanup contract (the repo's own copies live under
# eval/, which the npm package does not ship).
try_tmp_create() {
    local temp_root d
    temp_root="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || return 1
    d="$(mktemp -d "${temp_root}/loki-run.XXXXXXXX")" || return 1
    chmod 700 "$d" || { rmdir "$d" 2>/dev/null; return 1; }
    if ! printf '%s\n' "$d" >"$d/.loki-run-owned"; then rm -rf -- "$d"; return 1; fi
    printf '%s\n' "$d"
}

# Removes only a directory try_tmp_create made (loki-run.* child of the temp root whose
# .loki-run-owned marker holds its own path); the checks live in wow_cleanup_dir.
try_tmp_cleanup() {
    wow_cleanup_dir "${1:-}" loki-run .loki-run-owned
}

# Prints the starter task for the clone at $1 (source URL $2): the first open GitHub
# issue title when gh can read it, else the first TODO line. Prints nothing when neither exists.
try_pick_task() {
    local dest="$1" url="$2" slug title first p l t todo_file
    case "$url" in
        https://github.com/*)
            slug="${url#https://github.com/}"
            slug="${slug%.git}"
            slug="${slug%/}"
            if command -v gh >/dev/null 2>&1; then
                title="$(gh issue list --repo "$slug" --state open --limit 1 --json title --jq '.[0].title // empty' 2>/dev/null)" || title=""
                title="$(printf '%s' "$title" | LC_ALL=C tr -d '\000-\011\013-\037\177' | LC_ALL=C tr '\n' ' ')"
                if [ -n "$title" ]; then
                    printf '%s\n' "Resolve the open issue: ${title:0:200}"
                    return 0
                fi
            fi
            ;;
    esac
    todo_file="$(dirname -- "$dest")/todo.txt"
    git -C "$dest" grep -I -n -e TODO >"$todo_file" 2>/dev/null || true
    first="$(sed -n 1p "$todo_file" 2>/dev/null)"
    [ -n "$first" ] || return 0
    p="${first%%:*}"
    first="${first#*:}"
    l="${first%%:*}"
    t="${first#*:}"
    t="$(printf '%s' "$t" | LC_ALL=C tr -d '\000-\037\177' | sed -e 's/^[[:space:]]*//' | cut -c1-160)"
    p="$(printf '%s' "$p" | LC_ALL=C tr -d '\000-\037\177')"
    printf '%s\n' "Resolve the TODO in ${p} (line ${l}): ${t}"
}

# Copies the newest receipt.json under $1/.loki/runs/<run>/ to a kept location and prints its
# path. The clone is untrusted: every level (.loki, runs, the run dir, the file) is opened
# without following symlinks, and only a receipt modified after the stamp file $2 (touched
# right before loki start) is accepted, so a receipt committed in the repo is never reported.
try_keep_receipt() {
    local dest="$1" stamp="$2" out_root
    out_root="${LOKI_TRY_RECEIPT_DIR:-${HOME}/.loki/try-receipts}"
    python3 -I - "$dest" "$stamp" "$out_root" <<'PY'
import os, re, stat, sys
dest, stamp, out_root = sys.argv[1:4]
D = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
try:
    start = os.stat(stamp).st_mtime_ns
    root = os.open(dest, D)
    loki = os.open(".loki", D, dir_fd=root)
    runs = os.open("runs", D, dir_fd=loki)
    names = os.listdir(runs)
except OSError:
    sys.exit(1)
best = None
for name in names:
    if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name):
        continue
    try:
        rd = os.open(name, D, dir_fd=runs)
    except OSError:
        continue
    try:
        fd = os.open("receipt.json", os.O_RDONLY | os.O_NOFOLLOW, dir_fd=rd)
    except OSError:
        os.close(rd)
        continue
    try:
        st = os.fstat(fd)
        if stat.S_ISREG(st.st_mode) and st.st_size <= 16 * 1024 * 1024 and st.st_mtime_ns > start:
            if best is None or st.st_mtime_ns > best[0]:
                with os.fdopen(os.dup(fd), "rb") as f:
                    best = (st.st_mtime_ns, name, f.read())
    finally:
        os.close(fd)
        os.close(rd)
if best is None:
    sys.exit(1)
out = os.path.join(out_root, best[1])
os.makedirs(out, exist_ok=True)
target = os.path.join(out, "receipt.json")
with open(target, "wb") as f:
    f.write(best[2])
print(target)
PY
}

# One line per supported provider (names from SUPPORTED_PROVIDERS in providers/loader.sh), printed once.
try_print_provider_setup() {
    local want="$1" known p hint
    known="$(sed -n 's/^SUPPORTED_PROVIDERS=(\(.*\))$/\1/p' "$_LOKI_SCRIPT_DIR/../providers/loader.sh" 2>/dev/null | tr -d '"' | head -1)"
    if [ -z "$known" ]; then
        echo "loki try: cannot read SUPPORTED_PROVIDERS from providers/loader.sh" >&2
        return 1
    fi
    echo "loki try: no provider is ready for '$want'. Set up one of these, then run loki try again (or preview with --dry-run):"
    for p in $known; do
        case "$p" in
            claude) hint="claude login   (or set ANTHROPIC_API_KEY)" ;;
            codex) hint="codex login   (or set OPENAI_API_KEY)" ;;
            opencode) hint="opencode auth login" ;;
            aider) hint="install aider and set OPENAI_API_KEY or ANTHROPIC_API_KEY" ;;
            cline) hint="install the cline CLI and configure its API key" ;;
            *) hint="install the $p CLI (no login command is mapped for it)" ;;
        esac
        echo "  $p: $hint"
    done
}

_cmd_try_body() {
    local tmp="$1" url="$2" dry_run="$3" provider="$4" max_cost="$5"
    local dest="$tmp/repo" task
    if ! (
        unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_PREFIX
        GIT_TERMINAL_PROMPT=0 git -c protocol.ext.allow=never -c core.hooksPath=/dev/null \
            clone --depth 1 --quiet -- "$url" "$dest"
    ); then
        echo "loki try: could not clone $url" >&2
        return 1
    fi
    task="$(try_pick_task "$dest" "$url")"
    if [ -z "$task" ]; then
        echo "loki try: no starter task found (no open issue readable, no TODO line) in $url" >&2
        return 1
    fi
    echo "Clone path: $dest"
    echo "Task: $task"
    echo "Estimate: one task on the Loki 10 engine, provider $provider, cost capped at \$$max_cost (--max-cost); the receipt records the actual spend."
    if [ "$dry_run" = true ]; then
        echo "[dry-run] No model call was made. A real run would execute:"
        echo "  loki start \"<task>\" --no-pr --provider $provider --max-cost $max_cost"
        return 0
    fi
    local rc=0 receipt stamp="$tmp/started.stamp"
    : >"$stamp"
    (
        cd "$dest" && LOKI_ASSUME_YES=1 "$SKILL_DIR/bin/loki" start "$task" --no-pr --provider "$provider" --max-cost "$max_cost"
    ) || rc=$?
    if receipt="$(try_keep_receipt "$dest" "$stamp")"; then
        echo ""
        echo "Receipt: $receipt"
        echo "Check it: npx loki-mode verify \"$receipt\""
    else
        echo ""
        echo "No receipt was written (run exited $rc)."
        [ "$rc" != "0" ] || rc=1
    fi
    return "$rc"
}

cmd_try() {
    local url="" dry_run=false provider="" max_cost="2"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki try${NC} - Try Loki on a public repo without touching your checkout"
                echo ""
                echo "Usage: loki try <public-repo-url> [--dry-run] [--provider P] [--max-cost N]"
                echo ""
                echo "Clones the repo (depth 1) into a run-owned temp directory, picks a starter"
                echo "task (the first open GitHub issue title, else the first TODO line), runs it"
                echo "on the Loki 10 engine with no PR, keeps only the receipt, and always removes"
                echo "the temp clone. Your current checkout is never read or changed."
                echo ""
                echo "Options:"
                echo "  --dry-run     Clone, then print the clone path, the task and an estimate. No model call, no spend"
                echo "  --provider P  claude (default) or opencode, codex, cline, aider"
                echo "  --max-cost N  Hard per-run cap in USD, a positive number (default 2)"
                echo "  --help, -h    Show this help"
                echo ""
                echo "URL schemes: https://, http://, git://, file://"
                echo "The receipt is kept under \${LOKI_TRY_RECEIPT_DIR:-~/.loki/try-receipts}/<run>/receipt.json;"
                echo "re-check it with: loki verify <receipt path>"
                echo ""
                echo "Examples:"
                echo "  loki try https://github.com/owner/repo --dry-run"
                echo "  loki try https://github.com/owner/repo"
                return 0
                ;;
            --dry-run) dry_run=true; shift ;;
            --provider)
                provider="${2:-}"
                if [ -z "$provider" ]; then echo "loki try: --provider requires a value" >&2; return 2; fi
                shift 2
                ;;
            --provider=*) provider="${1#*=}"; shift ;;
            --max-cost)
                max_cost="${2:-}"
                if [ -z "$max_cost" ]; then echo "loki try: --max-cost requires a value" >&2; return 2; fi
                shift 2
                ;;
            --max-cost=*) max_cost="${1#*=}"; shift ;;
            -*)
                echo "loki try: unknown option: $1" >&2
                echo "Run 'loki try --help'." >&2
                return 2
                ;;
            *)
                if [ -n "$url" ]; then
                    echo "loki try: only one repo URL is accepted" >&2
                    return 2
                fi
                url="$1"
                shift
                ;;
        esac
    done
    if [ -z "$url" ]; then
        echo "Usage: loki try <public-repo-url> [--dry-run]" >&2
        return 2
    fi
    case "$url" in
        https://*|http://*|git://*|file://*) ;;
        *) echo "loki try: unsupported URL '$url' (use https://, http://, git:// or file://)" >&2; return 2 ;;
    esac
    case "$url" in *[[:space:]]*) echo "loki try: the URL must not contain whitespace" >&2; return 2 ;; esac
    if ! [[ "$max_cost" =~ ^[0-9]+(\.[0-9]+)?$ ]] || ! awk -v c="$max_cost" 'BEGIN { exit !(c > 0) }'; then
        echo "loki try: --max-cost must be a positive number of USD, got: $max_cost" >&2
        return 2
    fi
    local use_provider="${provider:-${LOKI_PROVIDER:-claude}}"
    case "$use_provider" in
        claude|opencode|codex|cline|aider) ;;
        *) echo "loki try: unknown provider '$use_provider' (use claude, opencode, codex, cline or aider)" >&2; return 2 ;;
    esac
    if [ "$dry_run" != true ] && ! wow_provider_ready "$use_provider"; then
        try_print_provider_setup "$use_provider" >&2
        return 2
    fi

    local tmp rc=0
    tmp="$(try_tmp_create)" || { echo "loki try: cannot create a temp directory" >&2; return 1; }
    # shellcheck disable=SC2064  # expand tmp now; it is local to this call
    trap "try_tmp_cleanup '$tmp'; trap - INT TERM HUP; exit 130" INT
    trap "try_tmp_cleanup '$tmp'; trap - INT TERM HUP; exit 143" TERM
    trap "try_tmp_cleanup '$tmp'; trap - INT TERM HUP; exit 129" HUP
    _cmd_try_body "$tmp" "$url" "$dry_run" "$use_provider" "$max_cost" || rc=$?
    trap - INT TERM HUP
    try_tmp_cleanup "$tmp"
    if [ ! -e "$tmp" ]; then echo "Removed the temp clone."; fi
    return "$rc"
}

# loki plan <task> --fast: a one-screen plan from the small model tier (at most
# 40 lines, acceptance clauses included), printed before the refine call starts,
# then the medium-tier refinement shown as a diff of at most 20 lines or "unchanged".
_plan_fast() {
    local task="$1"
    [ -f "$task" ] && task="$(cat "$task")"
    if [ -z "$task" ]; then
        echo -e "${RED}Usage: loki plan \"<task>\" --fast${NC}" >&2
        return 2
    fi
    local pdir="$_LOKI_SCRIPT_DIR/../providers"
    type load_provider >/dev/null 2>&1 || . "$pdir/loader.sh" 2>/dev/null
    if ! load_provider "${LOKI_PROVIDER:-claude}" >/dev/null 2>&1 || ! type provider_invoke_argv >/dev/null 2>&1; then
        echo "loki plan --fast: the provider '${LOKI_PROVIDER:-claude}' cannot be called from here" >&2
        return 1
    fi
    local tmp to="${LOKI_PLAN_FAST_TIMEOUT:-120}" tcmd=()
    command -v timeout >/dev/null 2>&1 && tcmd=(timeout "$to")
    tmp="$(mktemp -d "${TMPDIR:-/tmp}/loki-plan-fast.XXXXXXXX")" || return 1
    local common="Reply with plain text only. Do not use tools or edit files. No markdown fences."
    local p1="PLAN-FAST. $common Write a plan for this task in at most 30 short lines: numbered steps, then a line 'Acceptance:' followed by one clause per line, each a checkable statement.
Task: $task"
    provider_invoke_argv fast "$p1"
    "${tcmd[@]+"${tcmd[@]}"}" env CAVEMAN_DEFAULT_MODE=off "${_LOKI_INVOKE_ARGV[@]}" 2>/dev/null </dev/null \
        | sed '/^[[:space:]]*$/d' | sed -n '1,40p' > "$tmp/plan1.txt"
    if [ ! -s "$tmp/plan1.txt" ]; then
        echo "loki plan --fast: the small-tier model returned no plan" >&2
        rm -rf -- "$tmp"; return 1
    fi
    cat "$tmp/plan1.txt"
    local p2="PLAN-REFINE. $common Improve this plan: fix wrong or missing steps and make every acceptance clause checkable. Output the full improved plan in the same format, at most 40 lines.
Task: $task
Plan:
$(cat "$tmp/plan1.txt")"
    provider_invoke_argv development "$p2"
    "${tcmd[@]+"${tcmd[@]}"}" env CAVEMAN_DEFAULT_MODE=off "${_LOKI_INVOKE_ARGV[@]}" 2>/dev/null </dev/null \
        | sed '/^[[:space:]]*$/d' | sed -n '1,40p' > "$tmp/plan2.txt"
    echo ""
    echo "Refined:"
    if [ ! -s "$tmp/plan2.txt" ]; then
        echo "refine unavailable (kept the plan above)"
    elif diff "$tmp/plan1.txt" "$tmp/plan2.txt" >/dev/null 2>&1; then
        echo "unchanged"
    else
        diff "$tmp/plan1.txt" "$tmp/plan2.txt" | grep '^[<>]' | sed -n '1,20p' || true
    fi
    rm -rf -- "$tmp"
    return 0
}

cmd_plan() {
    local prd_file=""
    local show_requirements=false
    local show_json=false
    local show_verbose=false
    local fast=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki plan${NC} - Dry-run PRD analysis and cost estimation"
                echo ""
                echo "Usage: loki plan <PRD> [options]"
                echo ""
                echo "Analyzes a PRD file without executing anything. Outputs complexity,"
                echo "estimated iterations, token usage, cost, and execution plan."
                echo ""
                echo "Options:"
                echo "  --json        Machine-readable JSON output"
                echo "  --spec        With a task: write the acceptance criteria to .loki/specs/<slug>.md for you to edit"
                echo "  --requirements  Print the last run's requirement-to-check map (.loki/state/requirements-map.json), read-only"
                echo "  --fast        With a task: print a one-screen plan (max 40 lines, with acceptance clauses) from the small model tier, then the refined plan as a diff (max 20 lines) or 'unchanged'"
                echo "  --verbose     Show detailed per-iteration breakdown"
                echo "  --help, -h    Show this help"
                echo ""
                echo "Environment Variables:"
                echo "  LOKI_SESSION_MODEL            Pin cost estimate to a single tier"
                echo "                                (opus|sonnet|haiku|fable, default: sonnet)"
                echo "  LOKI_LEGACY_TIER_SWITCHING    Set to 'true' to restore the legacy"
                echo "                                Opus/Sonnet/Haiku per-iteration rotation"
                echo ""
                echo "Examples:"
                echo "  loki plan ./prd.md"
                echo "  loki plan ./prd.md --json"
                echo "  loki plan ./prd.md --verbose"
                return 0
                ;;
            --json) show_json=true; shift ;;
            --verbose) show_verbose=true; shift ;;
            --fast) fast=true; shift ;;
            --requirements) show_requirements=true; shift ;;
            *)
                if [ -z "$prd_file" ]; then
                    prd_file="$1"
                fi
                shift
                ;;
        esac
    done

    if [ "$show_requirements" = true ]; then
        _plan_show_requirements "$show_json"
        return $?
    fi

    if [ "$fast" = true ]; then
        _plan_fast "$prd_file"
        return $?
    fi

    if [ -z "$prd_file" ]; then
        # Under --json, a machine consumer expects JSON on every path, including
        # errors -- never ANSI-colored prose. Emit a structured error to stdout.
        if [ "$show_json" = true ]; then
            printf '{"error":"missing PRD file argument"}\n'
            return 2
        fi
        echo -e "${RED}Usage: loki plan <PRD file>${NC}"
        echo "Run 'loki plan --help' for usage."
        return 2
    fi

    if [ ! -f "$prd_file" ]; then
        if [ "$show_json" = true ]; then
            printf '{"error":"PRD file not found","prd_file":"%s"}\n' "$prd_file"
            return 1
        fi
        echo -e "${RED}PRD file not found: $prd_file${NC}"
        return 1
    fi

    local prd_path
    prd_path="$(cd "$(dirname "$prd_file")" && pwd)/$(basename "$prd_file")"

    show_prd_plan "$prd_path" "$show_json" "$show_verbose"
}

# Helper: decide whether to auto-show the plan from a major command.
# v6.81.1: default ON, opt-out via --no-plan. Also auto-skipped when stdout is
# not a TTY and --no-plan was not explicitly set (CI/automation contexts),
# logging a one-line "plan skipped: non-interactive" to stderr for visibility.
maybe_show_auto_plan() {
    local prd_path="$1"
    local no_plan_flag="${2:-false}"  # user passed --no-plan explicitly

    # Explicit opt-out wins.
    if [[ "$no_plan_flag" == "true" ]]; then
        return 0
    fi

    # PRD missing: silently skip (other commands will emit their own errors).
    if [ -z "$prd_path" ] || [ ! -f "$prd_path" ]; then
        return 0
    fi

    # Non-TTY heuristic: keep CI / pipe output clean. Log to stderr so it is
    # still surfaced in logs without corrupting stdout parsers.
    if [ ! -t 1 ]; then
        echo "plan skipped: non-interactive (pass --no-plan to silence, or run 'loki plan <PRD>' explicitly)" >&2
        return 0
    fi

    local abs_prd
    abs_prd="$(cd "$(dirname "$prd_path")" && pwd)/$(basename "$prd_path")"
    show_prd_plan "$abs_prd" "false" "false"
}

# Suggest the closest known command for a typo (the did-you-mean hint).
# Prints "Did you mean 'X'?" to stderr when an unknown command is within a
# small Levenshtein distance of a real command. Silent (returns 1) when
# nothing is close enough, so a genuinely-unknown command is not given a
# misleading suggestion. Pure bash + awk (no extra deps); the candidate list
# is the canonical set of top-level commands accepted by the dispatcher.
_suggest_command() {
    local typo="$1"
    [ -n "$typo" ] || return 1
    # Canonical top-level command names (keep in sync with the dispatch case
    # below). Deprecated aliases are intentionally included so a typo of an
    # alias still resolves to a helpful pointer.
    local known="start run quick plan grill spec verify proof trust review \
ultracode council demo dogfood heal modernize import issue github init template \
status stop pause resume monitor watch watchdog dashboard web open preview share \
memory context ctx checkpoint state report kpis cost metrics stats logs otel \
syslog telemetry crash config provider doctor setup setup-skill onboard quickstart try \
welcome update self-update rollback merge failover cluster enterprise remote deploy docker \
sandbox api mcp magic compound assets export wiki control docs explain why audit compliance \
welcome update self-update rollback failover cluster enterprise remote deploy docker \
sandbox api mcp acp magic compound assets export wiki control docs explain why audit compliance \
secrets analyze optimize bench ci reset cleanup notify trigger voice sentrux \
worktree wt projects cp rc trust-metrics serve agent code self_update test help version \
chat route agents models edit receipt setup diff undo"
    local best
    best=$(printf '%s\n' $known | awk -v t="$typo" '
        function min3(a, b, c) { if (a < b) { if (a < c) return a; return c } if (b < c) return b; return c }
        function lev(s1, s2,   n, m, i, j, prev, cur, cost, tmp) {
            n = length(s1); m = length(s2)
            if (n == 0) return m
            if (m == 0) return n
            for (j = 0; j <= m; j++) prev[j] = j
            for (i = 1; i <= n; i++) {
                cur[0] = i
                for (j = 1; j <= m; j++) {
                    cost = (substr(s1, i, 1) == substr(s2, j, 1)) ? 0 : 1
                    cur[j] = min3(prev[j] + 1, cur[j-1] + 1, prev[j-1] + cost)
                }
                for (j = 0; j <= m; j++) prev[j] = cur[j]
            }
            return prev[m]
        }
        { d = lev(t, $1); if (d < bestd || NR == 1) { bestd = d; bestc = $1 } }
        END {
            # Only suggest when the edit distance is small relative to the typo
            # length: <=2 for short commands, <=3 for longer ones. This avoids
            # absurd suggestions (e.g. "xyz" -> "web").
            thr = (length(t) <= 4) ? 2 : 3
            if (bestd <= thr) print bestc
        }')
    if [ -n "$best" ]; then
        printf '%s\n' "$best"
        return 0
    fi
    return 1
}

# Main command dispatcher
main() {
    # v7.89.0 telemetry TTY fix (council cH_r1 AC1): resolve interactivity EXACTLY
    # ONCE here, while autonomy/loki main still owns the user's real TTY, and
    # export the explicit signal that the telemetry/crash gates trust instead of a
    # late `-t` re-probe. When invoked via the bin/loki shim the var is already
    # exported (and re-exporting the same value is a no-op); this covers the path
    # where autonomy/loki is invoked directly (LOKI_LEGACY_BASH, no-Bun, or a
    # direct call). Only set when a terminal is actually present; leave UNSET
    # otherwise so isolated gate unit tests fall back to their own `-t` probe.
    if [ -z "${LOKI_TTY_INTERACTIVE:-}" ] && { [ -t 1 ] || [ -t 0 ]; }; then
        export LOKI_TTY_INTERACTIVE=1
    fi

    # v7.5.18: early guard -- LOKI_PROVIDER=gemini is no longer supported.
    if [ "${LOKI_PROVIDER:-}" = "gemini" ]; then
        echo -e "${RED}Error: Provider 'gemini' is deprecated as of v7.5.18 and has been removed.${NC}" >&2
        echo "Active providers: claude, codex, cline, aider, opencode" >&2
        echo "Unset LOKI_PROVIDER or use: LOKI_PROVIDER=claude" >&2
        exit 1
    fi

    if [ $# -eq 0 ] || { [ $# -eq 1 ] && [ "$1" = "--no-open" ]; }; then
        # bare `loki` starts the local UI. LOKI_LANDING=1 keeps the v7.28
        # newcomer landing (tests and scripts that parse it set it).
        if [ "${LOKI_LANDING:-0}" = "1" ]; then
            show_landing
            exit 0
        fi
        cmd_ui "$@"
        exit $?
    fi

    local command="$1"
    shift

    # LOKI_HELP_ONLY is set by `loki help <command>`, which delegates by
    # re-entering dispatch as `loki <command> --help`. Asking for help must
    # never DO anything, and dispatch reaches commands that start builds, stop
    # runs and modify the install. Every command honours --help today; this
    # makes that a guaranteed property of the help path rather than a fact
    # someone has to re-verify whenever a command is added. If the delegated
    # invocation somehow lost its --help, refuse rather than execute.
    if [ "${LOKI_HELP_ONLY:-0}" = "1" ]; then
        case " $* " in
            *" --help "*|*" -h "*) : ;;
            *)
                echo "loki: refusing to run '$command' from the help path" >&2
                return 1
                ;;
        esac
    fi

    # v7.4.13: first-run telemetry moved to bin/loki shim so it fires for
    # both Bun-routed and bash-routed commands (autonomy/loki main() never
    # runs for the 8 ported commands). Marker file: ~/.loki-first-run.
    #
    # v7.89.0 (council cH_r1 AC4): this is a FOREGROUND egress on the bash route.
    # Before it can phone home, the user must have seen the disclosure once -- a
    # bash-routed first command must never be covert. Evaluate the SAME gate the
    # egress uses, and if collection is genuinely enabled, disclose once (shared
    # impl from telemetry.sh, keyed on its own marker, so it never double-prints
    # with the Bun route or repeats). loki_telemetry itself re-checks the gate, so
    # this only adds the disclosure; it never changes whether we send.
    if declare -f _loki_telemetry_enabled >/dev/null 2>&1 \
       && declare -f _loki_disclose_telemetry_once >/dev/null 2>&1 \
       && _loki_telemetry_enabled; then
        _loki_disclose_telemetry_once
    fi
    # The command value is passed through the FIXED ALLOWLIST at the boundary
    # (_loki_known_command, autonomy/telemetry.sh): a real subcommand goes out
    # verbatim, anything else becomes the literal "other". Without this, the
    # honest typo `loki ./client-acme-merger-prd.md` put the raw file name on the
    # wire, because the payload builder is a pass-through dict, not an allowlist.
    # Fail closed: if the sanitizer is somehow undefined, send "other".
    if declare -f _loki_known_command >/dev/null 2>&1; then
        loki_telemetry "cli_command" "command=$(_loki_known_command "$command")" 2>/dev/null || true
    else
        loki_telemetry "cli_command" "command=other" 2>/dev/null || true
    fi

    # Unified config-file pre-pass (#691). For session commands ONLY, honor
    # LOKI_CONFIG_FILE / --config|--vars|--env-file and load the file BEFORE the
    # per-command arg loop runs. Running here means: config exports survive the
    # exec into run.sh, and the subsequent CLI arg loop still overrides them
    # (CLI > config). The pre-pass SCANS without consuming; the per-command
    # loops carry consume-and-ignore arms for the three aliases. Gated to
    # start/run/quick so status/config/stop never trigger a config load.
    # T5-D2 (FC-38 sweep): the bash entries have no attempts engine. --attempts (any value) is refused
    # here, before cmd_start / cmd_run / cmd_quick / cmd_demo can reach the legacy loop. bin/loki sends
    # `start --attempts N` to the Bun start command and never gets here with it.
    case "$command" in
        start|run|quick|demo|tour)
            local _att_a
            for _att_a in "$@"; do
                case "$_att_a" in
                    --attempts|--attempts=*)
                        echo -e "${RED}--attempts is only supported by 'loki start --attempts N --no-pr' (the Loki 10 engine); 'loki $command' would run the removed legacy loop.${NC}" >&2
                        exit 2
                        ;;
                esac
            done
            ;;
    esac

    case "$command" in
        start|run|quick)
            if declare -f loki_maybe_apply_config_file >/dev/null 2>&1; then
                loki_maybe_apply_config_file "$@"
            fi
            # --haiku is opt-in. The dump below shows the levers when the flag is present.
            loki_apply_haiku_optin "$@"
            loki_f2p_default "$command" "$@"
            # #691: documented observation hook. When LOKI_CONFIG_DUMP=1, print
            # the resolved LOKI_* environment (after the config pre-pass AND any
            # ambient env) and exit 0 WITHOUT running a build. Lets operators and
            # tests verify exactly which values a --config file resolves to,
            # without spend. The per-command CLI arg loops do not run on this
            # path, so CLI-flag overrides are NOT reflected by the dump; it shows
            # the config+env resolution only.
            if [ "${LOKI_CONFIG_DUMP:-}" = "1" ]; then
                env | grep -E '^LOKI_' | LC_ALL=C sort
                exit 0
            fi
            ;;
    esac

    case "$command" in
        run)
            cmd_run "$@"
            ;;
        start)
            cmd_start "$@"
            ;;
        quick)
            cmd_quick "$@"
            ;;
        chat)
            # shellcheck disable=SC1091
            source "$_LOKI_SCRIPT_DIR/lib/chat.sh"
            cmd_chat "$@"
            ;;
        edit)
            cmd_edit "$@"
            ;;
        diff)
            # Show what the last run changed: uncommitted changes, else the HEAD commit.
            # Extra args pass through to git (for example --stat or a path).
            if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
                echo "Usage: loki diff [git diff args]"
                echo "Show uncommitted changes; if there are none, show the latest commit."
                exit 0
            fi
            if ! git rev-parse --git-dir >/dev/null 2>&1; then
                echo "loki diff: not inside a git repository" >&2
                exit 1
            fi
            if git diff --quiet HEAD -- 2>/dev/null; then
                git show --format='%h %s' "$@" HEAD
            else
                git diff "$@" HEAD
            fi
            ;;
        monitor)
            cmd_monitor "$@"
            ;;
        demo)
            cmd_demo "$@"
            ;;
        wow)
            cmd_wow "$@"
            ;;
        factory)
            cmd_factory "$@"
            ;;
        tour)
            # Zero-key/zero-spend taste path: an alias for 'demo --offline' that
            # replays a bundled real Evidence Receipt. --help routes straight to
            # the demo help (which documents --offline / tour); --live is loki wow;
            # otherwise inject --offline ahead of any user args.
            if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
                cmd_demo --help
                echo "  loki tour --live                 # Tiny real task (same as loki wow)"
                echo "  loki wow                         # Live mini-task, or this sample with no key"
            elif [ "${1:-}" = "--live" ]; then
                shift
                cmd_wow "$@"
            else
                cmd_demo --offline "$@"
            fi
            ;;
        quickstart)
            cmd_quickstart "$@"
            ;;
        welcome)
            cmd_welcome "$@"
            ;;
        setup)
            # shellcheck disable=SC1091
            . "${SKILL_DIR}/autonomy/setup-wizard.sh"
            cmd_setup "$@"
            ;;
        init)
            cmd_init "$@"
            ;;
        stop)
            cmd_stop "$@"
            ;;
        cleanup)
            cmd_cleanup "$@"
            ;;
        pause)
            cmd_pause "$@"
            ;;
        steer)
            cmd_steer "$@"
            ;;
        cancel)
            cmd_cancel "$@"
            ;;
        resume)
            cmd_resume "$@"
            ;;
        status)
            cmd_status "$@"
            ;;
        why)
            cmd_why "$@"
            ;;
        next)
            cmd_next "$@"
            ;;
        stats)
            # CLI consolidation (Phase A): 'stats' is a deprecated alias of
            # 'report session'. On the Bun route this arm is never reached
            # (stats is Bun-native); the matching deprecation lives in
            # loki-ts/src/commands/stats.ts so both routes behave identically.
            _deprecated_alias stats "report session" "$@"
            cmd_report session "$@"
            ;;
        dashboard)
            cmd_dashboard "$@"
            ;;
        web)
            cmd_web "$@"
            ;;
        docker)
            cmd_docker "$@"
            ;;
        preview)
            cmd_preview "$@"
            ;;
        deploy)
            cmd_deploy "$@"
            ;;
        ship)
            cmd_ship "$@"
            ;;
        open)
            # CLI consolidation (Phase A): 'open' is a deprecated alias of 'preview'.
            _deprecated_alias open preview "$@"
            cmd_preview "$@"
            ;;
        logs)
            cmd_logs "$@"
            ;;
        serve)
            # CLI consolidation (Phase A): 'serve' is a deprecated alias of
            # 'api start'. Emit the pointer BEFORE the --help short-circuit so it
            # fires on every form (including 'serve --help'), consistent with the
            # other short aliases and testable without binding a TCP port.
            # Suppressed under --json/-q/--quiet by the helper.
            _deprecated_alias serve "api start" "$@"
            # v7.6.2 B-12 fix: 'serve --help' previously routed to 'cmd_api start --help'
            # which started the dashboard as a side effect instead of printing help.
            case "${1:-}" in
                --help|-h|help)
                    echo -e "${BOLD}Loki Mode -- start dashboard API server${NC}"
                    echo ""
                    echo "Usage: loki serve [options]"
                    echo ""
                    echo "Alias for 'loki api start'. Starts the dashboard HTTP API"
                    echo "server (FastAPI)."
                    echo ""
                    echo "For the UI, run 'loki' (opens the Control Plane) or 'loki control serve'."
                    echo ""
                    echo "Options:"
                    echo "  --host HOST    Bind host (default: 127.0.0.1)"
                    echo "  --port PORT    Bind port (default: LOKI_DASHBOARD_PORT)"
                    echo "  --help, -h     Show this help and exit"
                    echo ""
                    echo "Side effects: starts a long-running HTTP server bound to a TCP port."
                    echo "Stop with: loki dashboard stop"
                    return 0
                    ;;
            esac
            cmd_api start "$@"
            ;;
        api)
            cmd_api "$@"
            ;;
        sandbox)
            cmd_sandbox "$@"
            ;;
        notify)
            cmd_notify "$@"
            ;;
        import)
            cmd_import "$@"
            ;;
        github)
            cmd_github "$@"
            ;;
        issue)
            cmd_issue "$@"
            ;;
        watch)
            cmd_watch "$@"
            ;;
        cockpit)
            cmd_cockpit "$@"
            ;;
        export)
            # CLI consolidation (Phase A): 'export' -> 'report export'.
            _deprecated_alias export "report export" "$@"
            cmd_report export "$@"
            ;;
        assets)
            cmd_assets "$@"
            ;;
        backlog)
            cmd_backlog "$@"
            ;;
        workspace)
            cmd_workspace "$@"
            ;;
        config)
            cmd_config "$@"
            ;;
        provider)
            cmd_provider "$@"
            ;;
        reset)
            cmd_reset "$@"
            ;;
        memory)
            cmd_memory "$@"
            ;;
        compound)
            # CLI consolidation (Phase B): 'compound' -> 'memory compound'.
            _deprecated_alias compound "memory compound" "$@"
            cmd_memory compound "$@"
            ;;
        checkpoint|cp)
            # CLI consolidation (Phase A): 'cp' is a deprecated alias of 'checkpoint'.
            [ "$command" = "cp" ] && _deprecated_alias cp checkpoint "$@"
            cmd_checkpoint "$@"
            ;;
        rollback)
            cmd_rollback "$@"
            ;;
        backup)
            cmd_backup "$@"
            ;;
        restore)
            cmd_restore "$@"
            ;;
        merge)
            cmd_merge "$@"
            ;;
        council)
            cmd_council "$@"
            ;;
        dogfood)
            # CLI consolidation (Phase A): 'dogfood' -> 'report dogfood'.
            _deprecated_alias dogfood "report dogfood" "$@"
            cmd_report dogfood "$@"
            ;;
        projects)
            cmd_projects "$@"
            ;;
        enterprise)
            cmd_enterprise "$@"
            ;;
        voice)
            echo "Voice mode is planned for a future release. Track progress at github.com/asklokesh/loki-mode/issues/85"
            exit 0
            ;;
        secrets)
            cmd_secrets "$@"
            ;;
        doctor)
            cmd_doctor "$@"
            ;;
        models)
            bash "$SKILL_DIR/providers/model_routes.sh" "$@"
            ;;
        sentrux)
            cmd_sentrux "$@"
            ;;
        setup-skill)
            cmd_setup_skill "$@"
            ;;
        self-update|self_update|update)
            cmd_self_update "$@"
            ;;
        watchdog)
            cmd_watchdog "$@"
            ;;
        audit)
            cmd_audit "$@"
            ;;
        compliance)
            cmd_compliance "$@"
            ;;
        review)
            cmd_review "$@"
            ;;
        ultracode)
            cmd_ultracode "$@"
            ;;
        optimize)
            cmd_optimize "$@"
            ;;
        modernize)
            cmd_modernize "$@"
            ;;
        heal)
            # CLI consolidation (Phase B): 'heal' -> 'modernize heal'.
            _deprecated_alias heal "modernize heal" "$@"
            cmd_modernize heal "$@"
            ;;
        verify)
            cmd_verify "$@"
            ;;
        keys)
            # Public-key export lives in the Bun v10 engine. Delegate to it directly: exec'ing
            # bin/loki here loops forever under LOKI_LEGACY_BASH=1 (bin/loki execs this script back).
            _loki_bun_delegate engine10 keys "$@" || true
            echo "loki keys: needs the Bun engine, which is unavailable: ${_LOKI_DELEGATE_WHY:-bun route unavailable}" >&2
            [ -z "${_LOKI_DELEGATE_FIX:-}" ] || echo "Fix: ${_LOKI_DELEGATE_FIX}" >&2
            exit 1
            ;;
        spec)
            cmd_spec "$@"
            ;;
        intent)
            # The spec is an INTERPRETATION of what was wanted. This checks
            # whether it still matches the intent it was affirmed against.
            cmd_intent "$@"
            ;;
        grill)
            cmd_grill "$@"
            ;;
        mcp)
            cmd_mcp "$@"
            ;;
        runs)
            # R100-008: Bun-only; bin/loki routes here only when bun is absent.
            echo "loki runs needs the Bun runtime (install bun, then re-run: loki runs)" >&2
            exit 2
            ;;
        undo)
            # UNDO-1: Bun-only (verifies the receipt with the Bun verifier); bin/loki routes here only when bun is absent.
            echo "loki undo needs the Bun runtime (install bun, then re-run: loki undo <run-id> --plan)" >&2
            exit 2
            ;;
        acp)
            # ACP agent over stdio; stdout is protocol-only. See docs/acp-editors.md.
            LOKI_ACP_LOKI="${LOKI_ACP_LOKI:-$_LOKI_SCRIPT_DIR/loki}" exec python3 "$_LOKI_SCRIPT_DIR/acp-agent.py"
            ;;
        migrate)
            # CLI consolidation (Phase B): 'migrate' -> 'modernize migrate'.
            _deprecated_alias migrate "modernize migrate" "$@"
            cmd_modernize migrate "$@"
            ;;
        cluster)
            cmd_cluster "$@"
            ;;
        worktree|wt)
            # CLI consolidation (Phase A): 'wt' is a deprecated alias of 'worktree'.
            [ "$command" = "wt" ] && _deprecated_alias wt worktree "$@"
            cmd_worktree "$@"
            ;;
        agent)
            cmd_agent "$@"
            ;;
        agents)
            cmd_agents "$@"
            ;;
        template)
            cmd_template "$@"
            ;;
        state)
            cmd_state "$@"
            ;;
        metrics)
            # CLI consolidation (Phase A): 'metrics' -> 'report metrics'.
            _deprecated_alias metrics "report metrics" "$@"
            cmd_report metrics "$@"
            ;;
        cost)
            # CLI consolidation (Phase A): 'cost' -> 'report cost'.
            _deprecated_alias cost "report cost" "$@"
            cmd_report cost "$@"
            ;;
        estimate)
            cmd_estimate "$@"
            ;;
        trust)
            cmd_trust "$@"
            ;;
        trust-metrics)
            # CLI consolidation (Phase A): 'trust-metrics' -> 'trust detail'.
            _deprecated_alias trust-metrics "trust detail" "$@"
            cmd_trust detail "$@"
            ;;
        syslog)
            cmd_syslog "$@"
            ;;
        telemetry|otel)
            # CLI consolidation (Phase A): 'otel' is a deprecated alias of 'telemetry'.
            [ "$command" = "otel" ] && _deprecated_alias otel telemetry "$@"
            cmd_telemetry "$@"
            ;;
        remote|rc)
            # CLI consolidation (Phase A): 'rc' is a deprecated alias of 'remote'.
            [ "$command" = "rc" ] && _deprecated_alias rc remote "$@"
            cmd_remote "$@"
            ;;
        trigger)
            cmd_trigger "$@"
            ;;
        plan)
            cmd_plan "$@"
            ;;
        try)
            cmd_try "$@"
            ;;
        failover)
            cmd_failover "$@"
            ;;
        analyze)
            cmd_analyze "$@"
            ;;
        onboard)
            # CLI consolidation (Phase B): 'onboard' -> 'analyze onboard'.
            _deprecated_alias onboard "analyze onboard" "$@"
            cmd_analyze onboard "$@"
            ;;
        explain)
            # CLI consolidation (Phase B): 'explain' -> 'analyze explain'.
            _deprecated_alias explain "analyze explain" "$@"
            cmd_analyze explain "$@"
            ;;
        docs)
            cmd_docs "$@"
            ;;
        wiki)
            cmd_wiki "$@"
            ;;
        control)
            cmd_control "$@"
            ;;
        magic)
            cmd_magic "$@"
            ;;
        ci)
            cmd_ci "$@"
            ;;
        test)
            cmd_test "$@"
            ;;
        report)
            cmd_report "$@"
            ;;
        crash)
            cmd_crash "$@"
            ;;
        share)
            # CLI consolidation (Phase A): 'share' -> 'report share'.
            _deprecated_alias share "report share" "$@"
            cmd_report share "$@"
            ;;
        proof|receipt)
            # `loki receipt` is a friendly alias for `loki proof` (the Evidence
            # Receipt surface): same subcommands (list/show/verify/open/share).
            cmd_proof "$@"
            ;;
        outcomes)
            # The receipt says what was proven at the time. This says what
            # happened to it afterwards -- reverted, reworked, or survived.
            cmd_outcomes "$@"
            ;;
        secure)
            # Secure-by-default gate surface: inspect findings + manage waivers.
            cmd_secure "$@"
            ;;
        own|handoff)
            # Finish-and-own: a plain-English ownership handoff for a non-technical
            # owner (what was built, is it working, how to run/deploy, what is left).
            # `loki handoff` is an alias for `loki own`.
            cmd_own "$@"
            ;;
        bench)
            cmd_bench "$@"
            ;;
        context|ctx)
            # CLI consolidation (Phase B): 'context' (and short alias 'ctx')
            # -> 'analyze context'.
            _deprecated_alias "$command" "analyze context" "$@"
            cmd_analyze context "$@"
            ;;
        code)
            # CLI consolidation (Phase B): 'code' -> 'analyze code'.
            _deprecated_alias code "analyze code" "$@"
            cmd_analyze code "$@"
            ;;
        version|--version|-v)
            cmd_version "$@"
            ;;
	completions)
            cmd_completions "$@"
            ;;
        kpis)
            # CLI consolidation (Phase B): `kpis` is a deprecated alias of the
            # canonical `report kpis`. Emit the one-line pointer (suppressed
            # under --json/-q/--quiet by _deprecated_alias), then print the
            # honest Bun-requirement message via the shared helper. On a machine
            # WITH Bun, bin/loki routes `kpis` to the Bun runtime before this arm
            # is ever reached; this arm is the LOKI_LEGACY_BASH / no-Bun path.
            _deprecated_alias kpis "report kpis" "$@"
            _kpis_bash_unavailable "$@"
            ;;
        help|--help|-h)
            # CLI consolidation (Phase A): `loki help aliases` prints the full
            # deprecated-alias table; bare help prints the grouped front page.
            if [ "${1:-}" = "aliases" ]; then
                show_help_aliases
            elif [ "${1:-}" = "all" ]; then
                show_help_all
            elif [ -n "${1:-}" ] && case "${1:-}" in providers|exit-codes) true ;; *) false ;; esac && { source "$_LOKI_SCRIPT_DIR/lib/help-core.sh"; help_topic_text "$1"; }; then
                :
            elif [ -n "${1:-}" ]; then
                # `loki help <command>` reaches the command's own help. Before
                # this, $1 was read only for the literal "aliases" and otherwise
                # discarded, so `loki help proof` printed the same generic front
                # page as bare `loki help` -- while `loki proof --help` printed
                # real help. Both spellings now agree.
                #
                # LOKI_HELP_ONLY makes this safe by construction rather than by
                # audit. Delegation re-enters dispatch, and dispatch reaches
                # commands that start, stop and modify things. Every one of them
                # honours --help today, but "asking for help must never DO
                # anything" is a property worth enforcing at the boundary
                # instead of re-checking every time a command is added.
                local _help_target="$1"
                shift
                # FORK BOMB GUARD, and this is not hypothetical: it filled the
                # process table with 3,788 chained `bash autonomy/loki --help
                # --help` processes on this machine, after which even `pkill`
                # could not fork.
                #
                # `loki help --help` sets _help_target="--help" and delegates as
                # `"$0" --help --help`. That re-enters THIS case, and "$@"
                # accumulates one more --help every pass, so the recursion is
                # unbounded and spawns a process per level.
                #
                # LOKI_HELP_ONLY was described above as making delegation "safe
                # by construction". It does -- against SIDE EFFECTS. It says
                # nothing about SELF-REFERENCE, which is a different hazard: the
                # danger is not that a command does something, it is that the
                # help router asks itself for help forever.
                #
                # A help-shaped target has no command help to delegate to, so
                # print the front page directly instead of recursing.
                case "$_help_target" in
                    --help|-h|help|--h|-help)
                        show_help
                        ;;
                    *)
                        # Second, independent stop: if we are ALREADY inside a
                        # delegated help call, do not delegate again. Belt and
                        # braces, because the pattern list above is a blacklist
                        # and a future spelling would slip past it.
                        if [ "${LOKI_HELP_ONLY:-}" = "1" ]; then
                            show_help
                        elif [ "$_help_target" = "answer" ]; then
                            _help_answer
                        elif [ "$_help_target" = "verify-pr" ]; then
                            _help_verify_pr
                        else
                            LOKI_HELP_ONLY=1 "$0" "$_help_target" --help "$@"
                            local _help_rc=$?
                            if [ "$_help_target" = "verify" ] && [ "$_help_rc" -eq 0 ]; then
                                _help_verify_dsse
                            fi
                            return "$_help_rc"
                        fi
                        ;;
                esac
            else
                show_help
            fi
            ;;
        *)
            echo -e "${RED}Unknown command: $command${NC}" >&2
            local _suggestion
            if _suggestion=$(_suggest_command "$command"); then
                echo "Did you mean 'loki ${_suggestion}'?" >&2
            fi
            # Always point to the full command list, even with no near-miss match.
            echo "Run 'loki help' for the core commands, 'loki help all' for every command." >&2
            exit 2
            ;;
    esac
}
# v7.36.0 (issue #168): optional cloud multi-agent review tier (`loki review
# --ultra`). Wraps the upstream `claude ultrareview` subcommand. ALWAYS opt-in,
# cost-disclosed, default OFF, and advisory only (it does NOT block any gate).
#
# Consent model (modeled on cmd_demo's cost-confirm, autonomy/loki ~9488-9540):
#   - The disclosure ALWAYS prints (paid cloud op + may take up to 30 min).
#   - There is NO price API: we disclose the cost-CLASS, never a dollar figure.
#   - Interactive TTY without --yes: prompt, default NO.
#   - Non-TTY/CI without --yes (and no LOKI_ULTRAREVIEW=1): REFUSE with exit 2
#     and ZERO ultrareview calls (the no-silent-bill guard). Never hang on read.
#   - --yes or LOKI_ULTRAREVIEW=1 proceeds without prompting (this command only).
# Args: <format> <assume_yes> <timeout_minutes> <target> <trust_workspace>
_review_ultra() {
    local fmt="${1:-text}"
    local assume_yes="${2:-false}"
    local timeout_min="${3:-}"
    local target="${4:-}"
    local trust_ws="${5:-false}"

    # Make the ultrareview gate predicates available even though autonomy/loki
    # does not source claude-flags.sh globally (same on-demand pattern as
    # autonomy/lib/voter-agents.sh). Degrade honestly if the lib is missing.
    if ! declare -F loki_ultrareview_supported >/dev/null 2>&1; then
        local _ur_lib=""
        if [ -f "${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" ]; then
            _ur_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh"
        elif [ -f "$(dirname "$0")/lib/claude-flags.sh" ]; then
            _ur_lib="$(dirname "$0")/lib/claude-flags.sh"
        fi
        if [ -n "$_ur_lib" ]; then
            # shellcheck source=autonomy/lib/claude-flags.sh
            . "$_ur_lib" 2>/dev/null || true
        fi
    fi

    # 1. claude CLI present at all?
    if ! command -v claude >/dev/null 2>&1; then
        echo -e "${RED}Error: 'claude' CLI not found on PATH.${NC}" >&2
        echo "loki review --ultra requires the Claude Code CLI (2.1.x) for cloud review." >&2
        return 1
    fi

    # 2. Capability gate: does this claude support the ultrareview subcommand?
    #    If absent, honest message + clean exit, never a half-feature.
    if ! declare -F loki_ultrareview_supported >/dev/null 2>&1 \
       || ! loki_ultrareview_supported; then
        echo -e "${RED}Error: your 'claude' CLI does not support 'ultrareview'.${NC}" >&2
        echo "Cloud multi-agent review needs Claude Code 2.1.x or newer; please upgrade." >&2
        return 1
    fi

    # 3. Cost disclosure -- ALWAYS printed, even with --yes, so spend is never
    #    hidden. NO dollar figure (there is no price API); cost-CLASS only.
    echo -e "${BOLD}loki review --ultra${NC} - cloud multi-agent code review (optional, paid)" >&2
    echo "" >&2
    echo -e "${YELLOW}ultrareview is a PAID cloud operation billed by Anthropic, separate${NC}" >&2
    echo -e "${YELLOW}from local model spend. It may take up to 30 minutes.${NC}" >&2
    echo "Findings are advisory only and do not block Loki's completion gate." >&2
    echo "" >&2

    # 4. Consent. LOKI_ULTRAREVIEW=1 is the non-interactive opt-in equivalent of
    #    --yes for THIS command only (never a council auto-trigger).
    local proceed=false
    if [ "$assume_yes" = true ]; then
        proceed=true
    elif declare -F loki_ultrareview_enabled >/dev/null 2>&1 && loki_ultrareview_enabled; then
        proceed=true
    else
        # Interactive only when stdin is a real TTY and CI is not forcing
        # non-interactive (matches the project's first-run gate semantics).
        local ultra_interactive=true
        if [ ! -t 0 ] || [ -n "${CI:-}" ]; then
            ultra_interactive=false
        fi
        if [ "$ultra_interactive" = true ]; then
            local ans=""
            echo -n "Run cloud ultrareview now? [y/N] " >&2
            read -r ans </dev/tty 2>/dev/null || ans=""
            if [[ "$ans" =~ ^[Yy] ]]; then
                proceed=true
            else
                echo "Cancelled. Nothing was spent." >&2
                return 0
            fi
        else
            # Non-TTY/CI without --yes: never hang; refuse with exit 2 and make
            # ZERO ultrareview calls (the no-silent-bill guard).
            echo "ultrareview needs confirmation; re-run with --yes (or set LOKI_ULTRAREVIEW=1) to proceed non-interactively" >&2
            return 2
        fi
    fi

    [ "$proceed" = true ] || { echo "Cancelled. Nothing was spent." >&2; return 0; }

    # 5. Invoke `claude ultrareview [--json] [--timeout N] [target]` exactly once.
    local ur_argv=("ultrareview")
    if [ "$fmt" = "json" ]; then
        ur_argv+=("--json")
    fi
    if [ -n "$timeout_min" ]; then
        ur_argv+=("--timeout" "$timeout_min")
    fi
    # Claude Code 2.1.309+ needs --trust-workspace for scripted runs. Forward it
    # only when the user passed it AND the CLI lists it; never trust by default.
    local ur_trust_ok=false
    if { claude ultrareview --help 2>&1; claude --help 2>&1; } | grep -q -- '--trust-workspace'; then
        ur_trust_ok=true
    fi
    if [ "$trust_ws" = true ]; then
        if [ "$ur_trust_ok" = true ]; then
            ur_argv+=("--trust-workspace")
        else
            echo "Note: this claude CLI does not list --trust-workspace; not forwarding it." >&2
        fi
    fi
    if [ -n "$target" ]; then
        ur_argv+=("$target")
    fi

    echo -e "${DIM}Running: claude ${ur_argv[*]}${NC}" >&2
    echo "" >&2
    local ur_rc=0
    _loki_perm_argv_for "$PWD" >/dev/null 2>&1 || true
    SHELL="$_LOKI_CLAUDE_SHELL" claude "${ur_argv[@]}" || ur_rc=$?
    if [ "$ur_rc" -ne 0 ] && [ "$trust_ws" != true ] && [ "$ur_trust_ok" = true ]; then
        echo "If claude reported an untrusted folder, re-run: loki review --ultra --yes --trust-workspace" >&2
    fi
    return "$ur_rc"
}

# v7.38.0: optional native Claude Code Dynamic Workflow passthrough
# (`loki ultracode "<task>"`). Structurally identical to _review_ultra (issue
# #168): opt-in, capability-gated, cost-class disclosed, Claude-provider-only,
# clean exit on any unsupported path. PURE passthrough -- Loki adds NO
# orchestration of its own; it prepends the `ultracode` keyword to the prompt and
# routes through `claude -p`, which fires the workflow runtime.
#
# Consent model (mirrors _review_ultra):
#   - The cost-CLASS disclosure ALWAYS prints. There is NO price API, so we never
#     quote a dollar figure (that would be a lie).
#   - Interactive TTY without --yes: prompt, default NO.
#   - Non-TTY/CI without --yes (and no LOKI_USE_CLAUDE_WORKFLOWS=1): REFUSE with
#     exit 2 and ZERO workflow invocation (the no-silent-bill guard).
#   - --yes or LOKI_USE_CLAUDE_WORKFLOWS=1 proceeds without prompting.
# Args: <assume_yes> <task...>
_run_ultracode() {
    local assume_yes="${1:-false}"
    shift || true
    local task="$*"

    if [ -z "$task" ]; then
        echo -e "${RED}Error: loki ultracode requires a task description.${NC}" >&2
        echo "Usage: loki ultracode \"<task>\" [--yes]" >&2
        return 1
    fi

    # Make the workflow gate predicates available even though autonomy/loki does
    # not source claude-flags.sh globally (same on-demand pattern as
    # _review_ultra). Degrade honestly if the lib is missing.
    if ! declare -F loki_workflows_supported >/dev/null 2>&1; then
        local _wf_lib=""
        if [ -f "${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" ]; then
            _wf_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh"
        elif [ -f "$(dirname "$0")/lib/claude-flags.sh" ]; then
            _wf_lib="$(dirname "$0")/lib/claude-flags.sh"
        fi
        if [ -n "$_wf_lib" ]; then
            # shellcheck source=autonomy/lib/claude-flags.sh
            . "$_wf_lib" 2>/dev/null || true
        fi
    fi

    # 1. Provider gate: workflows are Claude-only. On Codex/Cline/Aider, print the
    #    honest message and exit CLEANLY (0) with ZERO invocation. Never break the
    #    other providers.
    local _provider="${LOKI_PROVIDER:-claude}"
    if [ "$_provider" != "claude" ]; then
        echo -e "${YELLOW}loki ultracode: Claude provider only.${NC}" >&2
        echo "Workflows need the Claude provider and claude CLI >= 2.1.154." >&2
        echo "Active provider is '${_provider}'; nothing was run." >&2
        return 0
    fi

    # 2. claude CLI present at all?
    if ! command -v claude >/dev/null 2>&1; then
        echo -e "${RED}Error: 'claude' CLI not found on PATH.${NC}" >&2
        echo "Workflows need the Claude provider and claude CLI >= 2.1.154." >&2
        return 1
    fi

    # 3. Capability gate: provider==claude AND CLI present AND version >= 2.1.154
    #    AND workflows not disabled. Honest message + clean exit if unsupported.
    if ! declare -F loki_workflows_supported >/dev/null 2>&1 \
       || ! loki_workflows_supported; then
        echo -e "${YELLOW}loki ultracode: workflows unavailable on this setup.${NC}" >&2
        echo "Workflows need the Claude provider and claude CLI >= 2.1.154" >&2
        echo "(and must not be disabled via CLAUDE_CODE_DISABLE_WORKFLOWS or the" >&2
        echo "disableWorkflows setting). Nothing was run." >&2
        return 0
    fi

    # 4. Cost-CLASS disclosure -- ALWAYS printed, even with --yes, so spend is
    #    never hidden. NO dollar figure (there is no price API); cost-CLASS only.
    echo -e "${BOLD}loki ultracode${NC} - native Claude Code Dynamic Workflow (optional)" >&2
    echo "" >&2
    echo -e "${YELLOW}Workflows spawn many agents and cost meaningfully more than a normal${NC}" >&2
    echo -e "${YELLOW}run; this is a Claude Code feature billed as normal usage.${NC}" >&2
    echo "" >&2

    # 5. Consent. LOKI_USE_CLAUDE_WORKFLOWS=1 is the non-interactive opt-in
    #    equivalent of --yes for THIS command.
    local proceed=false
    if [ "$assume_yes" = true ]; then
        proceed=true
    elif declare -F loki_workflows_enabled >/dev/null 2>&1 && loki_workflows_enabled; then
        proceed=true
    else
        local uc_interactive=true
        if [ ! -t 0 ] || [ -n "${CI:-}" ]; then
            uc_interactive=false
        fi
        if [ "$uc_interactive" = true ]; then
            local ans=""
            echo -n "Run the workflow now? [y/N] " >&2
            read -r ans </dev/tty 2>/dev/null || ans=""
            if [[ "$ans" =~ ^[Yy] ]]; then
                proceed=true
            else
                echo "Cancelled. Nothing was spent." >&2
                return 0
            fi
        else
            # Non-TTY/CI without --yes: never hang; refuse with exit 2 and make
            # ZERO workflow calls (the no-silent-bill guard).
            echo "workflows need confirmation; re-run with --yes (or set LOKI_USE_CLAUDE_WORKFLOWS=1) to proceed non-interactively" >&2
            return 2
        fi
    fi

    [ "$proceed" = true ] || { echo "Cancelled. Nothing was spent." >&2; return 0; }

    # 6. Invoke the Claude provider once with the prompt PREFIXED by "ultracode: "
    #    so the workflow runtime fires. ALWAYS opus per project policy (planning-
    #    grade fan-out). The keyword prefix is the only supported, stable trigger;
    #    Loki never authors a workflow script itself.
    local uc_prompt="ultracode: ${task}"
    local uc_argv=("-p" "$uc_prompt" "--model" "opus")

    echo -e "${DIM}Running: claude -p \"ultracode: ...\" --model opus${NC}" >&2
    echo "" >&2
    _loki_perm_argv_for "$PWD" >/dev/null 2>&1 || true
    SHELL="$_LOKI_CLAUDE_SHELL" claude "${uc_argv[@]}"
    return $?
}

# v7.38.0: `loki ultracode "<task>"` command. Thin opt-in surface that routes a
# task through a native Claude Code Dynamic Workflow. See _run_ultracode.
cmd_ultracode() {
    local uc_assume_yes=false
    local uc_args=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki ultracode${NC} - run a task as a native Claude Code Dynamic Workflow"
                echo ""
                echo "Usage: loki ultracode \"<task>\" [--yes]"
                echo ""
                echo "Prepends the 'ultracode' keyword to your task and routes it through"
                echo "the Claude provider, firing Claude Code's Dynamic Workflow runtime"
                echo "(many background subagents fanning out on a read/research/audit task)."
                echo ""
                echo "This is a PASSTHROUGH: Loki adds no orchestration of its own. It is"
                echo "OPT-IN, default OFF, and Claude-provider-only. The council, the 11"
                echo "quality gates, the evidence gate, and the RARV loop are untouched."
                echo ""
                echo "Requirements:"
                echo "  - Active provider is Claude (Tier 1)"
                echo "  - claude CLI >= 2.1.154"
                echo "  - Workflows not disabled (CLAUDE_CODE_DISABLE_WORKFLOWS / disableWorkflows)"
                echo "On any unmet requirement: an honest message prints and nothing runs."
                echo ""
                echo "Cost: workflows spawn many agents and cost meaningfully more than a"
                echo "normal run; this is a Claude Code feature billed as normal usage."
                echo "There is no price API, so no dollar figure is shown."
                echo ""
                echo "Options:"
                echo "  --yes, -y    Skip the confirmation prompt (consent to the cost)"
                echo "  --help, -h   Show this help"
                echo ""
                echo "Environment:"
                echo "  LOKI_USE_CLAUDE_WORKFLOWS=1   Non-interactive opt-in (like --yes)"
                echo "  CLAUDE_CODE_DISABLE_WORKFLOWS=1  Disable workflows entirely"
                echo ""
                echo "Examples:"
                echo "  loki ultracode \"audit this repo for dead code\""
                echo "  loki ultracode \"research the top 5 auth libraries for Node\" --yes"
                return 0
                ;;
            --yes|-y) uc_assume_yes=true; shift ;;
            --) shift; while [[ $# -gt 0 ]]; do uc_args+=("$1"); shift; done ;;
            *) uc_args+=("$1"); shift ;;
        esac
    done
    _run_ultracode "$uc_assume_yes" "${uc_args[@]}"
    return $?
}

# Standalone code review - diff-based quality gates (v6.20.0)
cmd_review() {
    local review_staged=false
    local review_pr=""
    local review_since=""
    local review_target=""
    local review_format="text"
    local review_severity="all"
    local review_ultra=false
    local review_trust_ws=false
    local review_assume_yes=false
    local review_timeout=""
    local review_risk=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki review${NC} - Standalone code review with quality gates"
                echo ""
                echo "Usage: loki review [options] [file-or-dir]"
                echo ""
                echo "Reviews code changes using static analysis, security scanning,"
                echo "style checks, and anti-pattern detection. No AI needed."
                echo ""
                echo "Sources (default: uncommitted changes via git diff):"
                echo "  --staged             Review staged changes only"
                echo "  --pr <number>        Review a GitHub PR"
                echo "  --since <commit>     Review changes since a commit"
                echo "  <file-or-dir>        Review specific files or directory"
                echo ""
                echo "Options:"
                echo "  --format json        Output as JSON (for CI integration)"
                echo "  --severity <level>   Filter: critical, high, medium, low, info (shows level+above)"
                echo "  --ultra              Run an OPTIONAL cloud multi-agent review via"
                echo "                       'claude ultrareview' (PAID cloud op, opt-in,"
                echo "                       default OFF, advisory only). Requires"
                echo "                       confirmation; --yes or LOKI_ULTRAREVIEW=1 to"
                echo "                       skip the prompt. Optional [file-or-dir] is"
                echo "                       passed as the ultrareview target (branch / PR"
                echo "                       number / base branch)."
                echo "  --risk               Deterministic 0-100 diff risk score with per-factor"
                echo "                       breakdown (files, sensitive paths, size, test delta,"
                echo "                       deleted tests). Works with --pr/--staged/--since;"
                echo "                       'loki review <pr-number> --risk' also works. Use --json"
                echo "  --yes, -y            Skip the --ultra confirmation prompt"
                echo "  --timeout <minutes>  --ultra: max minutes to wait (default 30)"
                echo "  --trust-workspace    --ultra: forward claude's --trust-workspace (needed for"
                echo "                       scripted runs in an untrusted folder; never default)"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Exit codes:"
                echo "  0  No HIGH or CRITICAL findings"
                echo "  1  HIGH severity findings present"
                echo "  2  CRITICAL severity findings present"
                echo ""
                echo "Examples:"
                echo "  loki review                     # Review uncommitted changes"
                echo "  loki review --staged             # Review staged changes"
                echo "  loki review --pr 42              # Review GitHub PR #42"
                echo "  loki review src/                 # Review all files in src/"
                echo "  loki review --since HEAD~5       # Changes in last 5 commits"
                echo "  loki review --format json        # JSON output for CI"
                echo "  loki review --severity high      # Only HIGH+ findings"
                echo "  loki review --ultra              # Cloud multi-agent review (paid, opt-in)"
                echo "  loki review --ultra 42           # Cloud review of GitHub PR #42"
                echo "  loki review --ultra --yes        # Skip confirmation (e.g. in a script)"
                return 0
                ;;
            --ultra) review_ultra=true; shift ;;
            --trust-workspace) review_trust_ws=true; shift ;;
            --risk) review_risk=true; shift ;;
            --json) review_format="json"; shift ;;
            --yes|-y) review_assume_yes=true; shift ;;
            --timeout)
                shift
                review_timeout="${1:-}"
                if [ -z "$review_timeout" ]; then
                    echo -e "${RED}Error: --timeout requires a value in minutes${NC}"
                    return 1
                fi
                shift
                ;;
            --staged) review_staged=true; shift ;;
            --pr)
                shift
                review_pr="${1:-}"
                if [ -z "$review_pr" ]; then
                    echo -e "${RED}Error: --pr requires a PR number${NC}"
                    return 1
                fi
                shift
                ;;
            --since)
                shift
                review_since="${1:-}"
                if [ -z "$review_since" ]; then
                    echo -e "${RED}Error: --since requires a commit reference${NC}"
                    return 1
                fi
                shift
                ;;
            --format)
                shift
                review_format="${1:-text}"
                shift
                ;;
            --severity)
                shift
                review_severity="${1:-all}"
                review_severity="$(echo "$review_severity" | tr '[:upper:]' '[:lower:]')"
                shift
                ;;
            -*) echo -e "${RED}Unknown option: $1${NC}"; return 1 ;;
            *) review_target="$1"; shift ;;
        esac
    done

    # v7.36.0 (issue #168): optional cloud multi-agent review tier. This is an
    # explicit, on-demand, opt-in, cost-disclosed path that wraps the existing
    # `claude ultrareview` subcommand. It short-circuits the local-only review
    # below: --ultra is a different operation (paid cloud), not a flag that
    # augments the local static analysis.
    if [ "$review_ultra" = true ]; then
        _review_ultra "$review_format" "$review_assume_yes" "$review_timeout" "$review_target" "$review_trust_ws"
        return $?
    fi

    if [ "$review_risk" = true ]; then
        if [ -z "$review_pr" ] && [ -z "$review_since" ] && [[ "$review_target" =~ ^[0-9]+$ ]] && [ ! -e "$review_target" ]; then
            review_pr="$review_target"
        fi
        local risk_diff risk_src="uncommitted changes"
        if [ -n "$review_pr" ]; then
            command -v gh &>/dev/null || { echo "Error: gh CLI required for PR risk" >&2; return 1; }
            risk_diff=$(gh pr diff "$review_pr" 2>&1) || { echo "Error: Could not fetch PR #${review_pr}: $risk_diff" >&2; return 1; }
            risk_src="PR #${review_pr}"
        elif [ -n "$review_since" ]; then
            risk_diff=$(git diff "$review_since" 2>&1) || { echo "Error: Invalid commit reference: $review_since" >&2; return 1; }
            risk_src="changes since $review_since"
        elif [ "$review_staged" = true ]; then
            risk_diff=$(git diff --cached 2>/dev/null); risk_src="staged changes"
        else
            risk_diff=$(git diff 2>/dev/null)
        fi
        local risk_args=()
        [ "$review_format" = "json" ] && risk_args+=(--json)
        printf '%s\n' "$risk_diff" | python3 "$_LOKI_SCRIPT_DIR/lib/review_risk.py" ${risk_args[@]+"${risk_args[@]}"} "$risk_src"
        return $?
    fi

    # Severity level ordering (higher number = more severe)
    _review_sev_level() {
        case "$1" in
            INFO) echo 1 ;;
            LOW) echo 2 ;;
            MEDIUM) echo 3 ;;
            HIGH) echo 4 ;;
            CRITICAL) echo 5 ;;
            *) echo 0 ;;
        esac
    }

    local min_sev=0
    case "$review_severity" in
        info) min_sev=1 ;;
        low) min_sev=2 ;;
        medium) min_sev=3 ;;
        high) min_sev=4 ;;
        critical) min_sev=5 ;;
        all) min_sev=0 ;;
    esac

    # Gather code to review
    local diff_content=""
    local review_files=""
    local source_desc=""

    if [ -n "$review_pr" ]; then
        # PR review via gh CLI
        if ! command -v gh &>/dev/null; then
            echo -e "${RED}Error: gh CLI required for PR review. Install: https://cli.github.com${NC}"
            return 1
        fi
        diff_content=$(gh pr diff "$review_pr" 2>&1) || {
            echo -e "${RED}Error: Could not fetch PR #${review_pr}: $diff_content${NC}"
            return 1
        }
        source_desc="PR #${review_pr}"
    elif [ -n "$review_since" ]; then
        diff_content=$(git diff "$review_since" 2>&1) || {
            echo -e "${RED}Error: Invalid commit reference: $review_since${NC}"
            return 1
        }
        source_desc="changes since $review_since"
    elif [ -n "$review_target" ]; then
        # File or directory target - get full contents
        if [ -d "$review_target" ]; then
            review_files=$(find "$review_target" -type f \
                \( -name "*.sh" -o -name "*.bash" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \
                -o -name "*.jsx" -o -name "*.tsx" -o -name "*.go" -o -name "*.rs" -o -name "*.rb" \
                -o -name "*.java" -o -name "*.c" -o -name "*.cpp" -o -name "*.h" \) \
                -not -path "*/node_modules/*" -not -path "*/.git/*" \
                -not -path "*/vendor/*" -not -path "*/__pycache__/*" \
                -not -path "*/.venv/*" -not -path "*/target/*" 2>/dev/null)
            source_desc="directory $review_target"
        elif [ -f "$review_target" ]; then
            review_files="$review_target"
            source_desc="file $review_target"
        else
            echo -e "${RED}Error: Not found: $review_target${NC}"
            return 1
        fi
    elif [ "$review_staged" = true ]; then
        diff_content=$(git diff --cached 2>/dev/null)
        source_desc="staged changes"
    else
        diff_content=$(git diff 2>/dev/null)
        source_desc="uncommitted changes"
    fi

    # Check if there is anything to review
    if [ -z "$diff_content" ] && [ -z "$review_files" ]; then
        if [ "$review_format" = "json" ]; then
            echo '{"source":"'"$source_desc"'","findings":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"total":0},"exit_code":0}'
        else
            echo -e "${GREEN}No changes to review ($source_desc).${NC}"
        fi
        return 0
    fi

    # Build file content for non-diff reviews
    local all_content=""
    if [ -n "$review_files" ]; then
        while IFS= read -r f; do
            [ -z "$f" ] && continue
            all_content+="=== FILE: $f ===
$(cat "$f" 2>/dev/null)
"
        done <<< "$review_files"
    else
        all_content="$diff_content"
    fi

    # Findings array: "file|line|severity|category|finding|suggestion"
    local findings=()
    local has_critical=false
    local has_high=false

    # Helper to add a finding
    _add_finding() {
        local file="$1" line="$2" sev="$3" cat="$4" finding="$5" suggestion="$6"
        local sev_num
        sev_num=$(_review_sev_level "$sev")
        if [ "$sev_num" -ge "$min_sev" ]; then
            findings+=("${file}|${line}|${sev}|${cat}|${finding}|${suggestion}")
            [ "$sev" = "CRITICAL" ] && has_critical=true || true
            [ "$sev" = "HIGH" ] && has_high=true || true
        fi
    }

    # --- Gate 1: Static Analysis ---
    # Run shellcheck on shell files if available
    if command -v shellcheck &>/dev/null; then
        local shell_files=""
        if [ -n "$review_files" ]; then
            shell_files=$(echo "$review_files" | grep -E '\.(sh|bash)$' || true)
        elif [ -n "$diff_content" ]; then
            shell_files=$(echo "$diff_content" | grep -E '^\+\+\+ b/' | sed 's|^+++ b/||' | grep -E '\.(sh|bash)$' || true)
        fi
        if [ -n "$shell_files" ]; then
            while IFS= read -r sf; do
                [ -z "$sf" ] && continue
                [ ! -f "$sf" ] && continue
                local sc_out
                sc_out=$(shellcheck -f gcc "$sf" 2>/dev/null || true)
                while IFS= read -r sc_line; do
                    [ -z "$sc_line" ] && continue
                    local sc_file sc_lineno sc_sev sc_msg
                    sc_file=$(echo "$sc_line" | cut -d: -f1)
                    sc_lineno=$(echo "$sc_line" | cut -d: -f2)
                    sc_sev=$(echo "$sc_line" | sed -n 's/.*: \(warning\|error\|note\|info\):.*/\1/p')
                    sc_msg=$(echo "$sc_line" | sed 's/.*: \(warning\|error\|note\|info\): //')
                    local mapped_sev="LOW"
                    case "$sc_sev" in
                        error) mapped_sev="HIGH" ;;
                        warning) mapped_sev="MEDIUM" ;;
                        *) mapped_sev="LOW" ;;
                    esac
                    _add_finding "$sc_file" "$sc_lineno" "$mapped_sev" "static-analysis" "$sc_msg" "Fix shellcheck finding"
                done <<< "$sc_out"
            done <<< "$shell_files"
        fi
    fi

    # Run eslint if available on JS/TS files
    if command -v npx &>/dev/null; then
        local js_files=""
        if [ -n "$review_files" ]; then
            js_files=$(echo "$review_files" | grep -E '\.(js|ts|jsx|tsx)$' || true)
        elif [ -n "$diff_content" ]; then
            js_files=$(echo "$diff_content" | grep -E '^\+\+\+ b/' | sed 's|^+++ b/||' | grep -E '\.(js|ts|jsx|tsx)$' || true)
        fi
        if [ -n "$js_files" ]; then
            while IFS= read -r jsf; do
                [ -z "$jsf" ] && continue
                [ ! -f "$jsf" ] && continue
                if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then
                    local eslint_out
                    eslint_out=$(npx eslint --format compact "$jsf" 2>/dev/null || true)
                    while IFS= read -r el; do
                        [ -z "$el" ] && continue
                        [[ "$el" == *"problem"* ]] && continue
                        local el_file el_line el_msg
                        el_file=$(echo "$el" | cut -d: -f1)
                        el_line=$(echo "$el" | cut -d: -f2)
                        el_msg=$(echo "$el" | sed 's/^[^)]*) //')
                        local el_sev="LOW"
                        [[ "$el" == *"Error"* ]] && el_sev="MEDIUM"
                        _add_finding "$el_file" "$el_line" "$el_sev" "static-analysis" "$el_msg" "Fix eslint finding"
                    done <<< "$eslint_out"
                fi
            done <<< "$js_files"
        fi
    fi

    # --- Gate 2: Security Scan ---
    # Helper: scan for a pattern across files or diff content
    _review_scan() {
        local pattern="$1" sev="$2" cat="$3" finding="$4" suggestion="$5"
        if [ -n "$review_files" ]; then
            # Scan actual files with grep -Hn (file:line:content)
            local scan_files_arr=()
            while IFS= read -r sf; do
                [ -z "$sf" ] && continue
                [ -f "$sf" ] && scan_files_arr+=("$sf")
            done <<< "$review_files"
            [ ${#scan_files_arr[@]} -eq 0 ] && return
            while IFS= read -r match; do
                [ -z "$match" ] && continue
                local mf ml
                mf=$(echo "$match" | cut -d: -f1)
                ml=$(echo "$match" | cut -d: -f2)
                _add_finding "$mf" "$ml" "$sev" "$cat" "$finding" "$suggestion"
            done < <(grep -HnE "$pattern" "${scan_files_arr[@]}" 2>/dev/null || true)
        else
            # Scan diff content (line numbers are from the text blob)
            while IFS= read -r match; do
                [ -z "$match" ] && continue
                local ml
                ml=$(echo "$match" | cut -d: -f1)
                _add_finding "diff" "$ml" "$sev" "$cat" "$finding" "$suggestion"
            done < <(echo "$all_content" | grep -nE "$pattern" 2>/dev/null || true)
        fi
    }

    # Hardcoded secrets
    local secret_patterns=(
        'API_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
        'SECRET_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
        'PASSWORD\s*[=:]\s*["\x27][^\x27"]{4,}'
        'PRIVATE_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
        'AWS_ACCESS_KEY_ID\s*[=:]\s*["\x27]AK[A-Z0-9]{18}'
        'ghp_[A-Za-z0-9]{36}'
        'sk-[A-Za-z0-9]{32,}'
        'Bearer\s+[A-Za-z0-9._-]{20,}'
    )
    for pattern in "${secret_patterns[@]}"; do
        _review_scan "$pattern" "CRITICAL" "security" \
            "Potential hardcoded secret detected" \
            "Use environment variables or a secrets manager instead"
    done

    # SQL injection patterns
    _review_scan '(SELECT|INSERT|UPDATE|DELETE|DROP)\s.*\+\s*(req\.|request\.|params\.|user)' \
        "HIGH" "security" \
        "Potential SQL injection: string concatenation in query" \
        "Use parameterized queries or prepared statements"

    # eval/exec usage
    _review_scan '(^|\s)(eval|exec)\s*\(' \
        "HIGH" "security" \
        "Dangerous eval/exec usage detected" \
        "Avoid eval/exec with dynamic input; use safer alternatives"

    # Unsafe deserialization
    _review_scan '(pickle\.loads?|yaml\.load\s*\()' \
        "HIGH" "security" \
        "Unsafe deserialization detected (pickle/yaml.load)" \
        "Use yaml.safe_load or avoid pickle with untrusted data"

    # --- Gate 3: Code Style ---
    # Long functions (>100 lines heuristic from diff)
    if [ -n "$review_files" ]; then
        while IFS= read -r f; do
            [ -z "$f" ] && continue
            [ ! -f "$f" ] && continue
            local line_count
            line_count=$(wc -l < "$f" | tr -d ' ')
            if [ "$line_count" -gt 500 ]; then
                _add_finding "$f" "1" "MEDIUM" "style" \
                    "File has $line_count lines (>500)" \
                    "Consider breaking into smaller modules"
            fi
            # Check for very long lines
            local long_lines
            long_lines=$(awk 'length > 200 {count++} END {print count+0}' "$f" 2>/dev/null)
            if [ "$long_lines" -gt 5 ]; then
                _add_finding "$f" "1" "LOW" "style" \
                    "$long_lines lines exceed 200 characters" \
                    "Break long lines for readability"
            fi
        done <<< "$review_files"
    fi

    # TODO/FIXME/HACK/XXX detection
    _review_scan '(TODO|FIXME|HACK|XXX):' "INFO" "style" \
        "TODO/FIXME/HACK/XXX comment marker found" \
        "Track in issue tracker instead of code comments"

    # --- Gate 4: Anti-pattern Detection ---
    # console.log left in code (JS/TS)
    _review_scan 'console\.(log|debug|warn)\(' "LOW" "anti-pattern" \
        "console.log statement (debug artifact?)" \
        "Remove debug logging or use a proper logger"

    # Catch bare except in Python
    _review_scan 'except\s*:' "MEDIUM" "anti-pattern" \
        "Bare except clause catches all exceptions" \
        "Catch specific exceptions (e.g., except ValueError)"

    # Disabled SSL verification
    _review_scan '(verify\s*=\s*False|VERIFY_SSL\s*=\s*False|NODE_TLS_REJECT_UNAUTHORIZED.*0|rejectUnauthorized.*false)' \
        "HIGH" "anti-pattern" \
        "SSL verification disabled" \
        "Enable SSL verification in production"

    # Hardcoded IP addresses (skip localhost/0.0.0.0/version patterns)
    _review_scan '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' \
        "LOW" "anti-pattern" \
        "Hardcoded IP address detected" \
        "Use configuration or DNS names instead"

    # --- Tally results ---
    local count_critical=0 count_high=0 count_medium=0 count_low=0 count_info=0
    for f in "${findings[@]+"${findings[@]}"}"; do
        local sev
        sev=$(echo "$f" | cut -d'|' -f3)
        case "$sev" in
            CRITICAL) count_critical=$((count_critical + 1)) ;;
            HIGH) count_high=$((count_high + 1)) ;;
            MEDIUM) count_medium=$((count_medium + 1)) ;;
            LOW) count_low=$((count_low + 1)) ;;
            INFO) count_info=$((count_info + 1)) ;;
        esac
    done
    local count_total=${#findings[@]}

    # Determine exit code
    local exit_code=0
    [ "$has_high" = true ] && exit_code=1 || true
    [ "$has_critical" = true ] && exit_code=2 || true

    # --- Output ---
    if [ "$review_format" = "json" ]; then
        local json_findings="["
        local first=true
        for f in "${findings[@]+"${findings[@]}"}"; do
            local f_file f_line f_sev f_cat f_finding f_suggestion
            f_file=$(echo "$f" | cut -d'|' -f1)
            f_line=$(echo "$f" | cut -d'|' -f2)
            f_sev=$(echo "$f" | cut -d'|' -f3)
            f_cat=$(echo "$f" | cut -d'|' -f4)
            f_finding=$(echo "$f" | cut -d'|' -f5)
            f_suggestion=$(echo "$f" | cut -d'|' -f6)
            # Escape quotes for JSON
            f_finding=$(echo "$f_finding" | sed 's/"/\\"/g')
            f_suggestion=$(echo "$f_suggestion" | sed 's/"/\\"/g')
            f_file=$(echo "$f_file" | sed 's/"/\\"/g')
            [ "$first" = true ] && first=false || json_findings+=","
            json_findings+="{\"file\":\"$f_file\",\"line\":$f_line,\"severity\":\"$f_sev\",\"category\":\"$f_cat\",\"finding\":\"$f_finding\",\"suggestion\":\"$f_suggestion\"}"
        done
        json_findings+="]"
        printf '{"source":"%s","findings":%s,"summary":{"critical":%d,"high":%d,"medium":%d,"low":%d,"info":%d,"total":%d},"exit_code":%d}\n' \
            "$source_desc" "$json_findings" "$count_critical" "$count_high" "$count_medium" "$count_low" "$count_info" "$count_total" "$exit_code"
    else
        echo -e "${BOLD}Loki Code Review${NC}"
        echo -e "Source: ${CYAN}$source_desc${NC}"
        echo "---"

        if [ "$count_total" -eq 0 ]; then
            echo -e "${GREEN}No findings. Code looks clean.${NC}"
        else
            # Group by severity
            for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
                local printed_header=false
                for f in "${findings[@]+"${findings[@]}"}"; do
                    local f_sev
                    f_sev=$(echo "$f" | cut -d'|' -f3)
                    [ "$f_sev" != "$sev_name" ] && continue
                    if [ "$printed_header" = false ]; then
                        local sev_color="$NC"
                        case "$sev_name" in
                            CRITICAL) sev_color="$RED" ;;
                            HIGH) sev_color="$RED" ;;
                            MEDIUM) sev_color="$YELLOW" ;;
                            LOW) sev_color="$CYAN" ;;
                            INFO) sev_color="$DIM" ;;
                        esac
                        echo ""
                        echo -e "${sev_color}${BOLD}[$sev_name]${NC}"
                        printed_header=true
                    fi
                    local f_file f_line f_cat f_finding f_suggestion
                    f_file=$(echo "$f" | cut -d'|' -f1)
                    f_line=$(echo "$f" | cut -d'|' -f2)
                    f_cat=$(echo "$f" | cut -d'|' -f4)
                    f_finding=$(echo "$f" | cut -d'|' -f5)
                    f_suggestion=$(echo "$f" | cut -d'|' -f6)
                    echo -e "  ${DIM}$f_file:$f_line${NC} [$f_cat] $f_finding"
                    echo -e "    -> $f_suggestion"
                done
            done
        fi

        echo ""
        echo "---"
        echo -e "Summary: ${RED}$count_critical critical${NC}, ${RED}$count_high high${NC}, ${YELLOW}$count_medium medium${NC}, ${CYAN}$count_low low${NC}, ${DIM}$count_info info${NC} ($count_total total)"

        if [ "$exit_code" -eq 2 ]; then
            echo -e "${RED}CRITICAL findings detected. Review required.${NC}"
        elif [ "$exit_code" -eq 1 ]; then
            echo -e "${RED}HIGH severity findings detected.${NC}"
        else
            echo -e "${GREEN}No HIGH or CRITICAL findings.${NC}"
        fi
    fi

    return "$exit_code"
}


# Worktree management (v6.7.0)
cmd_worktree() {
    local subcommand="${1:-list}"
    shift 2>/dev/null || true

    case "$subcommand" in
        list)
            echo -e "${BOLD}Worktree Status${NC}"
            echo ""

            # List git worktrees
            local worktrees
            worktrees=$(git worktree list --porcelain 2>/dev/null)
            if [ -z "$worktrees" ]; then
                echo "  No active worktrees"
                return 0
            fi

            local count=0
            local current_wt=""
            while IFS= read -r line; do
                case "$line" in
                    "worktree "*)
                        current_wt="${line#worktree }"
                        ;;
                    "branch "*)
                        local branch="${line#branch refs/heads/}"
                        local status="running"
                        local stream_name=""

                        # Check for merge signal (branches named parallel-<stream> by run.sh)
                        if [[ "$branch" == parallel-* ]] || [[ "$branch" == loki-parallel-* ]]; then
                            stream_name="${branch#parallel-}"
                            stream_name="${stream_name#loki-}"
                            if [ -f ".loki/signals/MERGE_REQUESTED_${stream_name}" ]; then
                                status="merge-ready"
                            elif [ -f ".loki/signals/WORKTREE_FAILED_${stream_name}" ]; then
                                status="failed"
                            fi
                        fi

                        # Skip main worktree
                        if [ "$current_wt" != "$(git rev-parse --show-toplevel 2>/dev/null)" ]; then
                            case "$status" in
                                merge-ready) echo -e "  ${GREEN}[merge-ready]${NC} $branch  ${DIM}$current_wt${NC}" ;;
                                failed)      echo -e "  ${RED}[failed]${NC}      $branch  ${DIM}$current_wt${NC}" ;;
                                *)           echo -e "  ${CYAN}[active]${NC}      $branch  ${DIM}$current_wt${NC}" ;;
                            esac
                            count=$((count + 1))
                        fi
                        current_wt=""
                        ;;
                esac
            done <<< "$worktrees"

            if [ "$count" -eq 0 ]; then
                echo "  No parallel worktrees (only main)"
            fi
            echo ""
            echo "  Total: $count worktree(s)"
            ;;

        merge)
            local name="${1:-}"
            if [ -z "$name" ]; then
                echo -e "${RED}Usage: loki worktree merge <name>${NC}"
                return 1
            fi

            echo -e "${BOLD}Merging worktree: $name${NC}"
            local signal_file=".loki/signals/MERGE_REQUESTED_${name}"
            if [ ! -f "$signal_file" ]; then
                echo -e "${RED}No merge signal found for: $name${NC}"
                echo "Use 'loki worktree list' to see available worktrees."
                return 1
            fi

            # Validate JSON is parseable and contains required fields
            if ! LOKI_SIGNAL_FILE="$signal_file" python3 << 'MERGE_VALIDATE_PY'
import json, sys, os
try:
    data = json.load(open(os.environ['LOKI_SIGNAL_FILE']))
    assert data.get('branch'), 'missing branch'
    assert data.get('worktree'), 'missing worktree'
except Exception as e:
    print(str(e), file=sys.stderr)
    sys.exit(1)
MERGE_VALIDATE_PY
            then
                echo -e "${RED}Invalid or corrupted merge signal file${NC}"
                return 1
            fi

            local branch
            branch=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['branch'])" 2>/dev/null)
            local worktree_path
            worktree_path=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['worktree'])" 2>/dev/null)

            # Validate branch was extracted successfully
            if [ -z "$branch" ]; then
                echo -e "${RED}Could not extract branch name from merge signal${NC}"
                return 1
            fi

            # Verify branch exists before attempting merge
            if ! git rev-parse --verify "$branch" &>/dev/null; then
                echo -e "${RED}Branch '$branch' does not exist${NC}"
                echo "The worktree may have been cleaned up already."
                rm -f "$signal_file"
                return 1
            fi

            if git merge --no-ff "$branch" -m "merge($name): auto-merge from parallel worktree"; then
                echo -e "${GREEN}Merge successful${NC}"
                rm -f "$signal_file"
                if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
                    git worktree remove "$worktree_path" --force 2>/dev/null || true
                    git branch -d "$branch" 2>/dev/null || true
                fi
            else
                echo -e "${RED}Merge conflict - resolve manually${NC}"
                git merge --abort 2>/dev/null || true
                return 1
            fi
            ;;

        clean)
            echo -e "${BOLD}Cleaning all worktrees...${NC}"
            local removed=0
            local main_wt
            main_wt=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
            # BUG-PU-001: Track branches to clean up after worktree removal
            local branches_to_delete=()
            local current_wt_clean=""
            local current_branch_clean=""
            while IFS= read -r line; do
                case "$line" in
                    "worktree "*)
                        current_wt_clean="${line#worktree }"
                        ;;
                    "branch "*)
                        current_branch_clean="${line#branch refs/heads/}"
                        if [ -n "$current_wt_clean" ] && [ "$current_wt_clean" != "$main_wt" ]; then
                            # Kill any running session in this worktree before removing
                            local wt_pid_file="$current_wt_clean/.loki/loki.pid"
                            if [ -f "$wt_pid_file" ]; then
                                local wt_pid
                                wt_pid=$(cat "$wt_pid_file" 2>/dev/null)
                                if [ -n "$wt_pid" ] && kill -0 "$wt_pid" 2>/dev/null; then
                                    echo "  Stopping session in $current_wt_clean (PID: $wt_pid)..."
                                    kill "$wt_pid" 2>/dev/null || true
                                    sleep 1
                                    kill -0 "$wt_pid" 2>/dev/null && kill -9 "$wt_pid" 2>/dev/null || true
                                fi
                            fi
                            echo "  Removing: $current_wt_clean"
                            git worktree remove "$current_wt_clean" --force 2>/dev/null || true
                            removed=$((removed + 1))
                            # Queue branch for deletion (can only delete after worktree is gone)
                            if [[ "$current_branch_clean" == loki-parallel-* ]] || \
                               [[ "$current_branch_clean" == parallel-* ]]; then
                                branches_to_delete+=("$current_branch_clean")
                            fi
                        fi
                        current_wt_clean=""
                        current_branch_clean=""
                        ;;
                esac
            done <<< "$(git worktree list --porcelain 2>/dev/null)"
            # Prune worktree metadata for any already-removed directories
            git worktree prune 2>/dev/null || true
            # Clean up orphaned parallel branches
            for branch in "${branches_to_delete[@]}"; do
                git branch -D "$branch" 2>/dev/null || true
            done
            rm -f .loki/signals/MERGE_REQUESTED_* .loki/signals/WORKTREE_FAILED_* 2>/dev/null || true
            echo -e "${GREEN}Cleanup complete ($removed worktrees removed)${NC}"
            ;;

        status)
            echo -e "${BOLD}Parallel Orchestrator Status${NC}"
            echo ""

            local merge_ready=0
            local failed=0
            local active=0

            for f in .loki/signals/MERGE_REQUESTED_*; do
                [ -f "$f" ] && merge_ready=$((merge_ready + 1))
            done
            for f in .loki/signals/WORKTREE_FAILED_*; do
                [ -f "$f" ] && failed=$((failed + 1))
            done

            local total_wt
            total_wt=$(git worktree list 2>/dev/null | wc -l | tr -d ' ')
            total_wt=$((total_wt - 1))  # Exclude main
            [ "$total_wt" -lt 0 ] && total_wt=0
            active=$((total_wt - merge_ready - failed))
            [ "$active" -lt 0 ] && active=0

            echo "  Active worktrees:  $active"
            echo "  Merge-ready:       $merge_ready"
            echo "  Failed:            $failed"
            echo "  Total:             $total_wt"
            ;;

        --help|-h|help)
            echo -e "${BOLD}loki worktree${NC} - Manage parallel worktrees"
            echo ""
            echo "Usage: loki worktree <command>"
            echo ""
            echo "Commands:"
            echo "  list       List active worktrees with status"
            echo "  merge NAME Merge a completed worktree back"
            echo "  clean      Remove all worktrees"
            echo "  status     Show parallel orchestrator state"
            echo ""
            ;;
        *)
            echo -e "${RED}Unknown worktree command: $subcommand${NC}"
            echo "Run 'loki worktree help' for usage."
            return 1
            ;;
    esac
}

# SQLite queryable state inspection
cmd_state() {
    local subcmd="${1:-help}"
    shift 2>/dev/null || true

    case "$subcmd" in
        --help|-h|help)
            echo -e "${BOLD}loki state${NC} - Query the SQLite state layer"
            echo ""
            echo "Usage: loki state <command> [options]"
            echo ""
            echo "Commands:"
            echo "  db                 Print path to SQLite database file"
            echo "  query events       Query events [--agent ID] [--type TYPE] [--limit N]"
            echo "  query messages     Query messages [--topic TOPIC] [--cluster ID] [--limit N]"
            echo "  query checkpoints  Query checkpoints [--migration ID] [--limit N]"
            echo ""
            echo "Examples:"
            echo "  loki state db"
            echo "  loki state query events --agent arch_001 --limit 20"
            echo "  loki state query messages --topic 'task.*'"
            echo "  loki state query checkpoints --migration mig_123"
            ;;
        db)
            local db_path="${LOKI_DATA_DIR:-${HOME}/.loki}/state.db"
            echo "$db_path"
            if [[ -f "$db_path" ]]; then
                local size
                size=$(ls -lh "$db_path" 2>/dev/null | awk '{print $5}')
                echo -e "${DIM}Size: ${size}${NC}"
            else
                echo -e "${YELLOW}Database not yet created${NC}"
            fi
            ;;
        query)
            local query_type="${1:-}"
            shift 2>/dev/null || true

            if [[ -z "$query_type" ]]; then
                echo -e "${RED}Usage: loki state query <events|messages|checkpoints> [options]${NC}"
                return 1
            fi

            local agent_id="" event_type="" topic="" cluster_id="" migration_id="" limit="20"
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --agent) agent_id="$2"; shift 2 ;;
                    --type) event_type="$2"; shift 2 ;;
                    --topic) topic="$2"; shift 2 ;;
                    --cluster) cluster_id="$2"; shift 2 ;;
                    --migration) migration_id="$2"; shift 2 ;;
                    --limit) limit="$2"; shift 2 ;;
                    *) shift ;;
                esac
            done

            PYTHONPATH="${SKILL_DIR:-.}" \
            LOKI_STATE_QUERY_TYPE="$query_type" \
            LOKI_STATE_AGENT_ID="$agent_id" \
            LOKI_STATE_EVENT_TYPE="$event_type" \
            LOKI_STATE_TOPIC="$topic" \
            LOKI_STATE_CLUSTER_ID="$cluster_id" \
            LOKI_STATE_MIGRATION_ID="$migration_id" \
            LOKI_STATE_LIMIT="$limit" \
            LOKI_STATE_SKILL_DIR="${SKILL_DIR:-.}" \
            python3 << 'STATE_QUERY_PY'
import json, sys, os
sys.path.insert(0, os.environ.get('LOKI_STATE_SKILL_DIR', '.'))
from state.sqlite_backend import SqliteStateBackend
db = SqliteStateBackend()

query_type = os.environ.get('LOKI_STATE_QUERY_TYPE', '')
agent_id = os.environ.get('LOKI_STATE_AGENT_ID', '') or None
event_type = os.environ.get('LOKI_STATE_EVENT_TYPE', '') or None
topic = os.environ.get('LOKI_STATE_TOPIC', '') or None
cluster_id = os.environ.get('LOKI_STATE_CLUSTER_ID', '') or None
migration_id = os.environ.get('LOKI_STATE_MIGRATION_ID', '') or None
limit = int(os.environ.get('LOKI_STATE_LIMIT', '20'))

if query_type == 'events':
    results = db.query_events(
        event_type=event_type,
        agent_id=agent_id,
        migration_id=migration_id,
        limit=limit
    )
elif query_type == 'messages':
    results = db.query_messages(
        topic=topic,
        cluster_id=cluster_id,
        limit=limit
    )
elif query_type == 'checkpoints':
    results = db.query_checkpoints(
        migration_id=migration_id,
        limit=limit
    )
else:
    print(f'Unknown query type: {query_type}')
    sys.exit(1)

if not results:
    print('No results found.')
else:
    for r in results:
        print(json.dumps(r, indent=2))
        print('---')
    print(f'{len(results)} result(s)')
STATE_QUERY_PY
            2>&1 || {
                echo -e "${RED}Error querying state database${NC}"
                return 1
            }
            ;;
        *)
            echo -e "${RED}Unknown state command: $subcmd${NC}"
            echo "Run 'loki state --help' for usage."
            return 1
            ;;
    esac
}

# Agent action audit log and quality scanning
# Resolve the repo root (where src/audit lives) from the loki script dir.
# _LOKI_SCRIPT_DIR is autonomy/; the JS audit modules live at ../src/audit.
_loki_audit_repo_root() {
    ( cd "$_LOKI_SCRIPT_DIR/.." && pwd )
}

# Link the run manifest (.loki/loki-run.json) into the agent audit chain for
# the TARGET project (not the loki repo). Honest no-op when the manifest is
# absent. Exit 0 on link or honest no-op; exit 1 only on a real error.
cmd_audit_link_manifest() {
    local target_dir="${1:-$PWD}"
    if ! command -v node &>/dev/null; then
        echo -e "${RED}Error: node is required for 'loki audit link-manifest'${NC}"
        echo "Install Node.js, then re-run."
        return 1
    fi
    local repo_root
    repo_root="$(_loki_audit_repo_root)"
    local abs_target
    abs_target="$( cd "$target_dir" 2>/dev/null && pwd )" || {
        echo -e "${RED}Error: project dir not found: $target_dir${NC}"
        return 1
    }

    echo -e "${BOLD}Linking run manifest into the audit chain...${NC}"
    echo "  Project: $abs_target"

    LOKI_AUDIT_MOD="$repo_root/src/audit" LOKI_AUDIT_TARGET="$abs_target" \
        node -e '
const audit = require(process.env.LOKI_AUDIT_MOD);
const res = audit.linkManifest({ projectDir: process.env.LOKI_AUDIT_TARGET });
if (res.linked) {
  console.log("  linked: yes");
  console.log("  manifest: " + res.manifestPath);
  console.log("  sha256: " + res.manifestSha256);
  if (res.manifestSchema) console.log("  schema: " + res.manifestSchema);
  if (res.anchor && typeof res.anchor.seq !== "undefined") console.log("  anchor seq: " + res.anchor.seq);
} else {
  console.log("  linked: no (" + (res.reason || "no-op") + ")");
  console.log("  manifest: " + res.manifestPath);
}
' || {
        echo -e "${RED}Error: manifest link failed${NC}"
        return 1
    }
    return 0
}

# Verify the run-manifest link for the TARGET project. Honest empty case
# (no anchor recorded yet) exits 0; a real tamper/integrity failure exits 1.
cmd_audit_verify_manifest() {
    local target_dir="${1:-$PWD}"
    if ! command -v node &>/dev/null; then
        echo -e "${RED}Error: node is required for 'loki audit verify-manifest'${NC}"
        echo "Install Node.js, then re-run."
        return 1
    fi
    local repo_root
    repo_root="$(_loki_audit_repo_root)"
    local abs_target
    abs_target="$( cd "$target_dir" 2>/dev/null && pwd )" || {
        echo -e "${RED}Error: project dir not found: $target_dir${NC}"
        return 1
    }

    echo -e "${BOLD}Verifying run-manifest link...${NC}"
    echo "  Project: $abs_target"

    LOKI_AUDIT_MOD="$repo_root/src/audit" LOKI_AUDIT_TARGET="$abs_target" \
        node -e '
const audit = require(process.env.LOKI_AUDIT_MOD);
const res = audit.verifyManifestLink({ projectDir: process.env.LOKI_AUDIT_TARGET });
const chainValid = res.chain && res.chain.valid;
console.log("  chain integrity: " + (chainValid ? "valid" : "INVALID"));
if (!res.present) {
  console.log("  manifest link: none recorded yet (no-op)");
  // Honest empty case: success only if the chain itself is valid.
  process.exit(chainValid ? 0 : 1);
}
const m = res.manifest || {};
console.log("  manifest: " + (m.manifestPath || "?"));
console.log("  pinned sha256:  " + (m.pinnedSha256 || "?"));
console.log("  current sha256: " + (m.currentSha256 || "(absent)"));
if (res.valid) {
  console.log("  verdict: VALID (manifest matches anchored hash)");
  process.exit(0);
} else {
  console.log("  verdict: TAMPERED / INVALID");
  if (m.error) console.log("  reason: " + m.error);
  process.exit(1);
}
'
    return $?
}

# loki compliance - periodic compliance snapshot (SOC2 / ISO27001 / GDPR).
# The scheduler is default-disabled (LOKI_COMPLIANCE_SNAPSHOT_INTERVAL_HOURS
# unset or 0). `loki compliance snapshot` runs the self-rate-limiting check;
# `--force` generates a snapshot now regardless of interval/elapsed gate.
cmd_compliance() {
    local subcommand="${1:-help}"

    case "$subcommand" in
        snapshot)
            shift
            local force="" target_dir="$PWD"
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --force|-f) force="1"; shift ;;
                    --help|-h)
                        echo -e "${BOLD}loki compliance snapshot${NC} - Generate/check a compliance snapshot"
                        echo ""
                        echo "Usage: loki compliance snapshot [PROJECT_DIR] [--force]"
                        echo ""
                        echo "Without --force, respects the default-disabled interval"
                        echo "(LOKI_COMPLIANCE_SNAPSHOT_INTERVAL_HOURS). Reports honestly:"
                        echo "  disabled     interval unset/0 (default)"
                        echo "  not-elapsed  interval not yet elapsed since last snapshot"
                        echo "  generated    a new snapshot was written"
                        echo ""
                        echo "With --force, a snapshot is generated now regardless."
                        echo "Snapshots are written to .loki/audit/compliance-snapshots/ in the project dir."
                        return 0
                        ;;
                    -*) echo -e "${RED}Unknown option: $1${NC}"; return 1 ;;
                    *) target_dir="$1"; shift ;;
                esac
            done

            if ! command -v node &>/dev/null; then
                echo -e "${RED}Error: node is required for 'loki compliance snapshot'${NC}"
                echo "Install Node.js, then re-run."
                return 1
            fi
            local repo_root
            repo_root="$(_loki_audit_repo_root)"
            local abs_target
            abs_target="$( cd "$target_dir" 2>/dev/null && pwd )" || {
                echo -e "${RED}Error: project dir not found: $target_dir${NC}"
                return 1
            }

            echo -e "${BOLD}Compliance snapshot${NC}"
            echo "  Project: $abs_target"

            LOKI_SCHED_MOD="$repo_root/src/audit/compliance-scheduler.js" \
            LOKI_SCHED_TARGET="$abs_target" LOKI_SCHED_FORCE="$force" \
                node -e '
const sched = require(process.env.LOKI_SCHED_MOD);
const target = process.env.LOKI_SCHED_TARGET;
const force = process.env.LOKI_SCHED_FORCE === "1";
if (force) {
  const now = Date.now();
  const snapshot = sched.buildSnapshot({ projectDir: target, nowMs: now });
  const file = sched.persistSnapshot({ projectDir: target, snapshot: snapshot, nowMs: now });
  console.log("  result: generated (forced)");
  console.log("  path: " + file);
  console.log("  audit entries: " + snapshot.totalAuditEntries);
  process.exit(0);
}
const res = sched.maybeGenerateSnapshot({ projectDir: target });
if (res.generated) {
  console.log("  result: generated");
  console.log("  path: " + res.path);
  console.log("  interval (hours): " + res.intervalHours);
} else if (res.reason === "disabled") {
  console.log("  result: disabled (scheduler off; default)");
  console.log("  enable: set LOKI_COMPLIANCE_SNAPSHOT_INTERVAL_HOURS=<N>, or use --force to generate now");
} else if (res.reason === "not-elapsed") {
  console.log("  result: not-elapsed (interval not yet reached)");
  console.log("  interval (hours): " + res.intervalHours);
  if (res.nextEligibleAtMs) console.log("  next eligible: " + new Date(res.nextEligibleAtMs).toISOString());
  console.log("  override: use --force to generate now");
} else {
  console.log("  result: " + (res.reason || "no-op"));
}
process.exit(0);
' || {
                echo -e "${RED}Error: compliance snapshot failed${NC}"
                return 1
            }
            return 0
            ;;
        --help|-h|help)
            echo -e "${BOLD}loki compliance${NC} - Periodic compliance snapshots"
            echo ""
            echo "Usage: loki compliance <subcommand> [options]"
            echo ""
            echo "Subcommands:"
            echo "  snapshot [DIR] [--force]   Generate/check a compliance snapshot"
            echo "  help                       Show this help"
            echo ""
            echo "The compliance scheduler is default-disabled. A snapshot bundles the"
            echo "SOC2, ISO27001 and GDPR reports plus the audit chain-integrity verdict"
            echo "into .loki/audit/compliance-snapshots/. Enable periodic generation by setting"
            echo "LOKI_COMPLIANCE_SNAPSHOT_INTERVAL_HOURS, or run with --force on demand."
            ;;
        *)
            echo -e "${RED}Unknown compliance subcommand: $subcommand${NC}"
            echo "Run 'loki compliance help' for usage."
            exit 1
            ;;
    esac
}

cmd_audit() {
    local subcommand="${1:-help}"
    local audit_file="$LOKI_DIR/logs/agent-audit.jsonl"

    case "$subcommand" in
        log)
            if [ ! -f "$audit_file" ]; then
                echo -e "${YELLOW}No audit log found at $audit_file${NC}"
                echo "Agent action auditing records entries during loki sessions."
                return 0
            fi
            local lines="${2:-50}"
            echo -e "${BOLD}Agent Audit Log${NC} (last $lines entries)"
            echo "---"
            tail -n "$lines" "$audit_file" | while IFS= read -r line; do
                if command -v python3 &>/dev/null; then
                    python3 -c "
import json, sys
try:
    e = json.loads(sys.argv[1])
    ts = e.get('timestamp', '?')
    act = e.get('action', '?')
    desc = e.get('description', '')
    it = e.get('iteration', 0)
    print(f'  [{ts}] [{act}] iter={it} {desc}')
except: print(f'  {sys.argv[1]}')
" "$line" 2>/dev/null || echo "  $line"
                else
                    echo "  $line"
                fi
            done
            ;;
        count)
            if [ ! -f "$audit_file" ]; then
                echo -e "${YELLOW}No audit log found${NC}"
                return 0
            fi
            echo -e "${BOLD}Agent Action Counts${NC}"
            echo "---"
            if command -v python3 &>/dev/null; then
                python3 -c "
import json, sys
from collections import Counter
counts = Counter()
for line in open(sys.argv[1]):
    try:
        e = json.loads(line)
        counts[e.get('action', 'unknown')] += 1
    except: pass
for action, count in sorted(counts.items(), key=lambda x: -x[1]):
    print(f'  {action:25s} {count}')
print(f'  {\"---\":25s} ---')
print(f'  {\"TOTAL\":25s} {sum(counts.values())}')
" "$audit_file" 2>/dev/null
            else
                echo "  python3 required for count summary"
            fi
            ;;
        scan)
            shift
            local preset="default"
            local do_export=""

            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --preset) if [[ -z "${2:-}" ]]; then echo -e "${RED}Error: --preset requires a value${NC}"; exit 1; fi; preset="$2"; shift 2 ;;
                    --preset=*) preset="${1#*=}"; shift ;;
                    --export) do_export="true"; shift ;;
                    --help|-h)
                        echo -e "${BOLD}loki audit scan${NC} - Run quality scan"
                        echo ""
                        echo "Usage: loki audit scan [options]"
                        echo ""
                        echo "Options:"
                        echo "  --preset NAME   Compliance preset (default|healthcare|fintech|government)"
                        echo "  --export        Save report to .loki/quality/report-{date}.json"
                        echo ""
                        return 0
                        ;;
                    *) echo -e "${RED}Unknown option: $1${NC}"; return 1 ;;
                esac
            done

            case "$preset" in
                default|healthcare|fintech|government) ;;
                *) echo -e "${RED}Error: Invalid preset '$preset'. Must be one of: default, healthcare, fintech, government${NC}"; return 1 ;;
            esac

            local port="${LOKI_DASHBOARD_PORT:-57374}"
            local host="127.0.0.1"
            local url="http://${host}:${port}/api/quality-scan?preset=${preset}"

            echo -e "${BOLD}Running quality scan...${NC} (preset: $preset)"
            echo ""

            local response http_code
            response=$(curl -s -w "\n%{http_code}" -X POST "$url" 2>/dev/null) || true
            http_code=$(echo "$response" | tail -1)
            response=$(echo "$response" | sed '$d')
            if [ -z "$http_code" ] || [ "$http_code" = "000" ]; then
                echo -e "${RED}Error: Could not connect to dashboard API at http://${host}:${port}${NC}"
                echo "Make sure the dashboard is running: loki serve"
                return 1
            fi
            if [ "$http_code" -ge 400 ] 2>/dev/null; then
                echo -e "${RED}Error: Dashboard API returned HTTP $http_code${NC}"
                [ -n "$response" ] && echo "$response"
                return 1
            fi

            if ! command -v python3 &>/dev/null; then
                echo "$response" | jq . 2>/dev/null || echo "$response"
            else
                echo "$response" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read())

score = data.get('score', 0)
grade = data.get('grade', '?')
print(f'Quality Score: {score}/100  Grade: {grade}')
print('---')

# Category scores
categories = data.get('categories', {})
if categories:
    print()
    print('Category Scores:')
    for cat, cat_score in sorted(categories.items()):
        print(f'  {cat:20s} {cat_score}/100')

# Findings by severity
findings = data.get('findings', [])
if findings:
    print()
    print('Findings:')
    for f in findings:
        sev = f.get('severity', 'info')
        msg = f.get('message', '')
        cat = f.get('category', '')
        if sev == 'critical':
            print(f'  \033[0;31m[CRITICAL]\033[0m {cat}: {msg}')
        elif sev == 'major':
            print(f'  \033[1;33m[MAJOR]\033[0m    {cat}: {msg}')
        else:
            print(f'  [MINOR]    {cat}: {msg}')
print()
" 2>/dev/null || echo "$response"
            fi

            # Export report if requested
            if [ -n "$do_export" ]; then
                local report_dir="$LOKI_DIR/quality"
                mkdir -p "$report_dir"
                local date_str
                date_str=$(date +%Y-%m-%d)
                local report_file="$report_dir/report-${date_str}.json"

                local report_url="http://${host}:${port}/api/quality-report?format=json"
                local report_data
                report_data=$(curl -sf "$report_url" 2>/dev/null) || true
                if [ -n "$report_data" ]; then
                    echo "$report_data" > "$report_file"
                    echo -e "${GREEN}Report exported to $report_file${NC}"
                else
                    # Fall back to scan response
                    echo "$response" > "$report_file"
                    echo -e "${GREEN}Report exported to $report_file${NC}"
                fi
            fi
            ;;
        --preset|--export)
            # Handle flags passed directly to 'loki audit' (shortcut for 'loki audit scan')
            cmd_audit scan "$@"
            return
            ;;
        lint)
            echo -e "${BOLD}Running static analysis...${NC}"
            echo ""
            local findings=0
            local target="${2:-.}"

            # JavaScript/TypeScript
            if [ -f "$target/package.json" ]; then
                echo -e "${CYAN}JavaScript/TypeScript:${NC}"
                local js_files
                js_files=$(find "$target" -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" | grep -v node_modules | grep -v dist | head -50)
                if [ -n "$js_files" ]; then
                    if [ -f "$target/.eslintrc.js" ] || [ -f "$target/.eslintrc.json" ] || [ -f "$target/.eslintrc.yml" ] || [ -f "$target/eslint.config.js" ] || [ -f "$target/eslint.config.mjs" ]; then
                        npx eslint $js_files 2>&1 | head -50
                        [ ${PIPESTATUS[0]} -ne 0 ] && findings=$((findings + 1))
                    else
                        echo "  No ESLint config found - running syntax check"
                        for f in $js_files; do
                            node --check "$f" 2>&1 || findings=$((findings + 1))
                        done
                    fi
                fi
            fi

            # Python
            if [ -f "$target/setup.py" ] || [ -f "$target/pyproject.toml" ] || [ -f "$target/requirements.txt" ]; then
                echo -e "${CYAN}Python:${NC}"
                local py_files
                py_files=$(find "$target" -name "*.py" | grep -v __pycache__ | grep -v .venv | grep -v venv | head -50)
                if [ -n "$py_files" ]; then
                    if command -v ruff &>/dev/null; then
                        ruff check $py_files 2>&1 | head -50
                        [ ${PIPESTATUS[0]} -ne 0 ] && findings=$((findings + 1))
                    else
                        echo "  ruff not available - running py_compile"
                        for f in $py_files; do
                            python3 -m py_compile "$f" 2>&1 || findings=$((findings + 1))
                        done
                    fi
                fi
            fi

            # Shell scripts
            local sh_files
            sh_files=$(find "$target" -name "*.sh" | grep -v node_modules | head -20)
            if [ -n "$sh_files" ]; then
                echo -e "${CYAN}Shell:${NC}"
                if command -v shellcheck &>/dev/null; then
                    shellcheck $sh_files 2>&1 | head -50
                    [ ${PIPESTATUS[0]} -ne 0 ] && findings=$((findings + 1))
                else
                    echo "  shellcheck not available - running bash -n"
                    for f in $sh_files; do
                        bash -n "$f" 2>&1 || findings=$((findings + 1))
                    done
                fi
            fi

            echo ""
            if [ "$findings" -gt 0 ]; then
                echo -e "${RED}Static analysis found issues ($findings checks failed)${NC}"
                return 1
            else
                echo -e "${GREEN}Static analysis passed${NC}"
                return 0
            fi
            ;;

        test)
            echo -e "${BOLD}Running test coverage check...${NC}"
            echo ""
            local target="${2:-.}"
            local min_coverage="${LOKI_MIN_COVERAGE:-80}"
            local test_failed=0

            # JavaScript/TypeScript (jest/vitest)
            if [ -f "$target/package.json" ]; then
                local test_cmd=""
                if grep -q '"vitest"' "$target/package.json" 2>/dev/null; then
                    test_cmd="npx vitest run --coverage"
                elif grep -q '"jest"' "$target/package.json" 2>/dev/null; then
                    test_cmd="npx jest --coverage --passWithNoTests"
                fi
                if [ -n "$test_cmd" ]; then
                    echo -e "${CYAN}Running: $test_cmd${NC}"
                    (cd "$target" && eval "$test_cmd") 2>&1
                    [ $? -ne 0 ] && test_failed=1
                fi
            fi

            # Python (pytest)
            if [ -f "$target/setup.py" ] || [ -f "$target/pyproject.toml" ]; then
                if command -v pytest &>/dev/null; then
                    echo -e "${CYAN}Running: pytest --cov${NC}"
                    (cd "$target" && pytest --cov 2>&1) || test_failed=1
                fi
            fi

            # Go
            if [ -f "$target/go.mod" ]; then
                echo -e "${CYAN}Running: go test -cover${NC}"
                (cd "$target" && go test -cover ./... 2>&1) || test_failed=1
            fi

            # Rust
            if [ -f "$target/Cargo.toml" ]; then
                echo -e "${CYAN}Running: cargo test${NC}"
                (cd "$target" && cargo test 2>&1) || test_failed=1
            fi

            echo ""
            if [ "$test_failed" -ne 0 ]; then
                echo -e "${RED}Test coverage gate FAILED${NC}"
                return 1
            else
                echo -e "${GREEN}Tests passed (min coverage TARGET, not measured: ${min_coverage}%)${NC}"
                return 0
            fi
            ;;

        link-manifest)
            # Fold the run manifest (.loki/loki-run.json bill-of-materials) into
            # the agent audit chain so it becomes tamper-evident and verifiable
            # against the evidence chain. Explicit, on-demand command: cmd_start
            # execs run.sh and replaces the loki process, so there is no
            # post-completion return point in this CLI wrapper to auto-invoke
            # from. Honest no-op (exit 0) when the manifest is absent.
            shift
            local target_dir="${1:-$PWD}"
            cmd_audit_link_manifest "$target_dir"
            ;;
        verify-manifest)
            # Verify the run-manifest link: agent chain integrity AND the on-disk
            # manifest still matching the hash pinned by the most recent
            # manifest-link anchor. Honest empty case (exit 0) when no anchor was
            # recorded yet; nonzero only on a real tamper/integrity failure.
            shift
            local target_dir="${1:-$PWD}"
            cmd_audit_verify_manifest "$target_dir"
            ;;
        --help|-h|help)
            echo -e "${BOLD}loki audit${NC} - Agent audit log and quality scanning"
            echo ""
            echo "Usage: loki audit <subcommand> [options]"
            echo ""
            echo "Subcommands:"
            echo "  log [N]            Show last N audit log entries (default: 50)"
            echo "  count              Count actions by type"
            echo "  scan               Run quality scan against dashboard API"
            echo "  lint               Run static analysis on project files"
            echo "  test               Run test coverage check"
            echo "  link-manifest [P]  Link the run manifest into the audit chain (tamper-evidence)"
            echo "  verify-manifest [P] Verify the run-manifest link against the evidence chain"
            echo "  help               Show this help"
            echo ""
            echo "Quality Scan Options (loki audit scan):"
            echo "  --preset NAME   Compliance preset (default|healthcare|fintech|government)"
            echo "  --export        Save report to .loki/quality/report-{date}.json"
            echo ""
            echo "Manifest tamper-evidence (loki audit link-manifest / verify-manifest):"
            echo "  Hashes .loki/loki-run.json (the build bill-of-materials) into the"
            echo "  agent audit chain. link-manifest no-ops honestly if the manifest is"
            echo "  absent; verify-manifest reports honestly if no anchor exists yet."
            echo "  Optional [P] is the project dir (default: current directory)."
            echo ""
            echo "The agent audit log records actions taken during Loki sessions,"
            echo "including CLI invocations, git commits, and session lifecycle events."
            echo "Log file: $audit_file"
            ;;
        *)
            echo -e "${RED}Unknown audit subcommand: $subcommand${NC}"
            echo "Run 'loki audit help' for usage."
            exit 1
            ;;
    esac
}

# Reset session state
cmd_reset() {
    require_jq || return 1

    local subcommand="${1:-all}"

    case "$subcommand" in
        retries|retry)
            if [ -f "$LOKI_DIR/autonomy-state.json" ]; then
                # Reset only retry count
                if command -v python3 &> /dev/null; then
                    python3 -c "
import json, sys, os
p = os.path.join(sys.argv[1], 'autonomy-state.json')
with open(p, 'r') as f:
    state = json.load(f)
state['retryCount'] = 0
state['status'] = 'reset'
with open(p, 'w') as f:
    json.dump(state, f, indent=4)
" "$LOKI_DIR" 2>/dev/null
                    echo -e "${GREEN}Retry count reset to 0${NC}"
                else
                    rm -f "$LOKI_DIR/autonomy-state.json"
                    echo -e "${GREEN}Autonomy state cleared${NC}"
                fi
            else
                echo -e "${YELLOW}No autonomy state found${NC}"
            fi
            ;;
        failed|failures)
            if [ -f "$LOKI_DIR/queue/failed.json" ]; then
                local count
                count=$(jq 'length' "$LOKI_DIR/queue/failed.json" 2>/dev/null || echo "?")
                # An empty failed.json makes jq exit 0 with no output, so the
                # `|| echo "?"` fallback never fires; normalize the blank result.
                [ -n "$count" ] || count="?"
                rm -f "$LOKI_DIR/queue/failed.json"
                echo "[]" > "$LOKI_DIR/queue/failed.json"
                echo -e "${GREEN}Cleared $count failed tasks${NC}"
            else
                echo -e "${YELLOW}No failed tasks found${NC}"
            fi
            ;;
        all)
            # Reset everything
            rm -f "$LOKI_DIR/autonomy-state.json"
            echo "[]" > "$LOKI_DIR/queue/failed.json" 2>/dev/null
            echo -e "${GREEN}Session state reset${NC}"
            echo "  - Autonomy state cleared"
            echo "  - Failed task queue cleared"
            ;;
        --help|-h|help)
            echo -e "${BOLD}loki reset${NC} - Reset session state"
            echo ""
            echo "Usage: loki reset [target]"
            echo ""
            echo "Targets:"
            echo "  all       Reset everything (default)"
            echo "  retries   Reset only the retry counter"
            echo "  failed    Clear failed task queue"
            echo ""
            echo "Examples:"
            echo "  loki reset              # Reset all state"
            echo "  loki reset retries      # Just reset retry count"
            echo "  loki reset failed       # Clear failed tasks"
            ;;
        *)
            echo -e "${RED}Unknown reset target: $subcommand${NC}"
            echo "Valid targets: all, retries, failed"
            exit 1
            ;;
    esac
}

# Cross-project memory/learnings management
cmd_memory() {
    local subcommand="${1:-list}"
    local learnings_dir="${HOME}/.loki/learnings"

    # Ensure directory exists
    mkdir -p "$learnings_dir"

    case "$subcommand" in
        list|ls)
            echo -e "${BOLD}Cross-Project Learnings${NC}"
            echo ""

            local patterns=0 mistakes=0 successes=0
            [ -f "$learnings_dir/patterns.jsonl" ] && patterns=$(grep -c '"description"' "$learnings_dir/patterns.jsonl" 2>/dev/null) || patterns=0
            [ -f "$learnings_dir/mistakes.jsonl" ] && mistakes=$(grep -c '"description"' "$learnings_dir/mistakes.jsonl" 2>/dev/null) || mistakes=0
            [ -f "$learnings_dir/successes.jsonl" ] && successes=$(grep -c '"description"' "$learnings_dir/successes.jsonl" 2>/dev/null) || successes=0

            echo -e "  Patterns:  ${GREEN}$patterns${NC}"
            echo -e "  Mistakes:  ${YELLOW}$mistakes${NC}"
            echo -e "  Successes: ${CYAN}$successes${NC}"
            echo ""
            echo "Location: $learnings_dir"
            echo ""
            echo "Use 'loki memory show <type>' to view entries"
            ;;

        show)
            # Parse arguments: show <type> [--limit N] [--project P]
            local type="${2:-all}"
            local limit=20
            local project=""
            shift 2 2>/dev/null || true

            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --limit|-l)
                        limit="${2:-20}"
                        if [ $# -ge 2 ]; then shift 2; else shift; fi
                        ;;
                    --project|-p)
                        project="${2:-}"
                        if [ $# -ge 2 ]; then shift 2; else shift; fi
                        ;;
                    *)
                        shift
                        ;;
                esac
            done

            case "$type" in
                patterns|pattern)
                    echo -e "${BOLD}Patterns${NC}"
                    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
                    if [ -f "$learnings_dir/patterns.jsonl" ]; then
                        python3 -c "
import json, sys, os
limit = int(sys.argv[1])
project_filter = sys.argv[2]
filepath = os.path.join(sys.argv[3], 'patterns.jsonl')
count = 0
with open(filepath, 'r') as f:
    for line in f:
        if count >= limit:
            break
        try:
            e = json.loads(line)
            if 'description' in e:
                if project_filter and e.get('project', '') != project_filter:
                    continue
                print(f\"[{e.get('project', 'unknown')}] {e['description'][:100]}\")
                count += 1
        except: pass
if count == 0:
    print('(empty - no patterns recorded)')
" "$limit" "$project" "$learnings_dir" 2>/dev/null
                    else
                        echo "(empty - no patterns recorded)"
                    fi
                    ;;

                mistakes|mistake)
                    echo -e "${BOLD}Mistakes${NC}"
                    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
                    if [ -f "$learnings_dir/mistakes.jsonl" ]; then
                        python3 -c "
import json, sys, os
limit = int(sys.argv[1])
project_filter = sys.argv[2]
filepath = os.path.join(sys.argv[3], 'mistakes.jsonl')
count = 0
with open(filepath, 'r') as f:
    for line in f:
        if count >= limit:
            break
        try:
            e = json.loads(line)
            if 'description' in e:
                if project_filter and e.get('project', '') != project_filter:
                    continue
                print(f\"[{e.get('project', 'unknown')}] {e['description'][:100]}\")
                count += 1
        except: pass
if count == 0:
    print('(empty - no mistakes recorded)')
" "$limit" "$project" "$learnings_dir" 2>/dev/null
                    else
                        echo "(empty - no mistakes recorded)"
                    fi
                    ;;

                successes|success)
                    echo -e "${BOLD}Successes${NC}"
                    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
                    if [ -f "$learnings_dir/successes.jsonl" ]; then
                        python3 -c "
import json, sys, os
limit = int(sys.argv[1])
project_filter = sys.argv[2]
filepath = os.path.join(sys.argv[3], 'successes.jsonl')
count = 0
with open(filepath, 'r') as f:
    for line in f:
        if count >= limit:
            break
        try:
            e = json.loads(line)
            if 'description' in e:
                if project_filter and e.get('project', '') != project_filter:
                    continue
                print(f\"[{e.get('project', 'unknown')}] {e['description'][:100]}\")
                count += 1
        except: pass
if count == 0:
    print('(empty - no successes recorded)')
" "$limit" "$project" "$learnings_dir" 2>/dev/null
                    else
                        echo "(empty - no successes recorded)"
                    fi
                    ;;

                all)
                    cmd_memory show patterns --limit "$limit" --project "$project"
                    echo ""
                    cmd_memory show mistakes --limit "$limit" --project "$project"
                    echo ""
                    cmd_memory show successes --limit "$limit" --project "$project"
                    ;;

                *)
                    echo -e "${RED}Unknown type: $type${NC}"
                    echo "Valid types: patterns, mistakes, successes, all"
                    return 1
                    ;;
            esac
            ;;

        search)
            local query="${2:-}"
            if [ -z "$query" ]; then
                echo -e "${RED}Usage: loki memory search <query>${NC}"
                return 1
            fi

            echo -e "${BOLD}Memory Search: $query${NC}"
            echo ""

            # Use python3.12 for ML packages (3.14 lacks sentence-transformers)
            local _search_py="python3"
            if command -v /opt/homebrew/bin/python3.12 &>/dev/null; then
                _search_py="/opt/homebrew/bin/python3.12"
            elif command -v python3.12 &>/dev/null; then
                _search_py="python3.12"
            fi

            # BUG-PU-010: Pass query via environment variable instead of embedding
            # directly in heredoc to prevent shell/Python injection via triple-quotes
            export LOKI_MEM_QUERY="$query"
            $_search_py << 'PYEOF'
import os, json, sys, glob

query = os.environ.get('LOKI_MEM_QUERY', '')
results = []

# Keyword search across all memory files
memory_dirs = [
    '.loki/memory/episodic',
    '.loki/memory/semantic',
    '.loki/memory/skills',
    '.loki/memory/handoffs',
]

for mdir in memory_dirs:
    if not os.path.isdir(mdir):
        continue
    for fpath in glob.glob(os.path.join(mdir, '*.json')):
        try:
            with open(fpath) as f:
                data = json.load(f)
            text = json.dumps(data).lower()
            if query.lower() in text:
                # Score by number of matches
                score = text.count(query.lower())
                title = data.get('title', data.get('task_id', data.get('name', os.path.basename(fpath))))
                category = os.path.basename(mdir)
                results.append((score, category, title, fpath))
        except Exception:
            continue

# Also search markdown files
for mdir in ['.loki/memory', '.loki']:
    if not os.path.isdir(mdir):
        continue
    for fpath in glob.glob(os.path.join(mdir, '*.md')):
        try:
            with open(fpath) as f:
                text = f.read().lower()
            if query.lower() in text:
                score = text.count(query.lower())
                title = os.path.basename(fpath)
                results.append((score, 'docs', title, fpath))
        except Exception:
            continue

# Try vector search if available
vector_results = []
try:
    sys.path.insert(0, '.')
    from memory.retrieval import MemoryRetrieval
    from memory.storage import MemoryStorage
    storage = MemoryStorage('.loki/memory')
    retriever = MemoryRetrieval(storage)
    retriever.build_indices()
    for coll in ['episodic', 'semantic', 'skills']:
        try:
            vr = retriever.retrieve_by_similarity(query, coll, top_k=3)
            for item in vr:
                if isinstance(item, dict):
                    item['_collection'] = coll
                vector_results.append(item)
        except Exception:
            continue
    if vector_results:
        print("  [Vector Search Results]")
        for item in vector_results[:5]:
            if isinstance(item, dict):
                coll = item.get('_collection', '?')
                title = item.get('title', item.get('name', item.get('id', '?')))
                summary = item.get('summary', item.get('pattern', ''))[:80]
                print(f"    [{coll}] {title}")
                if summary:
                    print(f"           {summary}")
            else:
                print(f"    {str(item)[:80]}")
        print()
except ImportError:
    pass  # Vector search not available
except Exception:
    pass

# Show keyword results
if results:
    results.sort(key=lambda x: -x[0])
    print("  [Keyword Search Results]")
    for score, category, title, fpath in results[:10]:
        print(f"    [{category}] {title}  ({score} matches)")
        print(f"           {fpath}")
    print()
    print(f"  Found: {len(results)} result(s)")
else:
    if not vector_results:
        print("  No results found for: " + query)
        print("  Tip: Try broader search terms")
PYEOF
            ;;

        clear)
            local type="${2:-}"
            local confirm=""

            if [ -z "$type" ]; then
                # LOKI_AUTO_CONFIRM takes precedence when explicitly set;
                # fall back to CI env var only when LOKI_AUTO_CONFIRM is unset
                local _auto_confirm="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
                if [[ "$_auto_confirm" == "true" ]]; then
                    confirm="yes"
                else
                    echo -e "${YELLOW}This will delete ALL cross-project learnings.${NC}"
                    echo -n "Are you sure? (yes/no): "
                    read -r confirm
                fi
                if [ "$confirm" = "yes" ]; then
                    rm -rf "$learnings_dir"
                    mkdir -p "$learnings_dir"
                    echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/patterns.jsonl"
                    echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/mistakes.jsonl"
                    echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/successes.jsonl"
                    echo -e "${GREEN}All learnings cleared${NC}"
                else
                    echo "Cancelled"
                fi
            else
                case "$type" in
                    patterns|mistakes|successes)
                        echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/${type}.jsonl"
                        echo -e "${GREEN}Cleared $type${NC}"
                        ;;
                    *)
                        echo -e "${RED}Unknown type: $type${NC}"
                        echo "Valid types: patterns, mistakes, successes"
                        return 1
                        ;;
                esac
            fi
            ;;

        export)
            local output="${2:-learnings-export.json}"

            # BUG-PU-010: pass the output filename via an environment variable
            # rather than interpolating $output into the python -c source, so a
            # filename containing quotes/backslashes/newlines cannot break out of
            # the string literal or inject python.
            LOKI_LEARNINGS_DIR="$learnings_dir" LOKI_LEARNINGS_OUT="$output" python3 -c "
import json
import os

learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
result = {'patterns': [], 'mistakes': [], 'successes': []}

for category in ['patterns', 'mistakes', 'successes']:
    filepath = os.path.join(learnings_dir, f'{category}.jsonl')
    if os.path.exists(filepath):
        with open(filepath, 'r') as f:
            for line in f:
                try:
                    e = json.loads(line)
                    if 'description' in e:
                        result[category].append(e)
                except: pass

_out = os.environ['LOKI_LEARNINGS_OUT']
with open(_out, 'w') as f:
    json.dump(result, f, indent=2)
print(f'Exported to {_out}')
" 2>/dev/null
            ;;

        stats)
            echo -e "${BOLD}Learning Statistics${NC}"
            echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

            LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
import json
import os
from collections import Counter

learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
projects = Counter()
categories = Counter()

for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
    filepath = os.path.join(learnings_dir, filename)
    if not os.path.exists(filepath):
        continue

    category = filename.replace('.jsonl', '')
    with open(filepath, 'r') as f:
        for line in f:
            try:
                e = json.loads(line)
                if 'description' in e:
                    projects[e.get('project', 'unknown')] += 1
                    categories[category] += 1
            except: pass

print('By Category:')
for cat, count in categories.most_common():
    print(f'  {cat}: {count}')

print()
print('By Project:')
for proj, count in projects.most_common(10):
    print(f'  {proj}: {count}')
" 2>/dev/null
            ;;

        dedupe|dedup)
            echo -e "${BOLD}Deduplicating Learnings${NC}"
            echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

            LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
import json
import os
import hashlib
from datetime import datetime, timezone

learnings_dir = os.environ['LOKI_LEARNINGS_DIR']

def dedupe_file(filepath):
    '''Deduplicate a JSONL file, keeping first occurrence of each unique description'''
    if not os.path.exists(filepath):
        return 0, 0

    seen_hashes = set()
    unique_entries = []
    duplicates = 0

    # Read all entries
    with open(filepath, 'r') as f:
        for line in f:
            try:
                entry = json.loads(line)
                if 'description' not in entry:
                    # Keep header/metadata lines
                    if 'version' in entry:
                        unique_entries.append(entry)
                    continue

                # Normalize description for hashing (case-insensitive, trimmed)
                desc = entry['description'].strip().lower()
                h = hashlib.md5(desc.encode()).hexdigest()

                if h not in seen_hashes:
                    seen_hashes.add(h)
                    unique_entries.append(entry)
                else:
                    duplicates += 1
            except json.JSONDecodeError:
                continue

    # Write back unique entries
    with open(filepath, 'w') as f:
        for entry in unique_entries:
            f.write(json.dumps(entry) + '\n')

    return len(unique_entries) - (1 if unique_entries and 'version' in unique_entries[0] else 0), duplicates

total_kept = 0
total_removed = 0

for category in ['patterns', 'mistakes', 'successes']:
    filepath = os.path.join(learnings_dir, f'{category}.jsonl')
    kept, removed = dedupe_file(filepath)
    total_kept += kept
    total_removed += removed
    if removed > 0:
        print(f'{category}: kept {kept}, removed {removed} duplicates')
    else:
        print(f'{category}: {kept} entries (no duplicates)')

print()
print(f'Total: kept {total_kept} unique entries, removed {total_removed} duplicates')
if total_removed > 0:
    reduction = total_removed / (total_kept + total_removed) * 100
    print(f'Storage reduced by {reduction:.1f}%')
" 2>/dev/null
            ;;

        index)
            # Show or rebuild the memory index layer
            if [ "${2:-}" = "rebuild" ]; then
                PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
import sys
try:
    from memory.engine import MemoryEngine
    engine = MemoryEngine(base_path='.loki/memory')
    engine.rebuild_index()
    total = (engine.get_index() or {}).get('total_memories', 0)
except Exception as e:
    print(f'Error: index rebuild failed: {type(e).__name__}: {e}', file=sys.stderr)
    sys.exit(1)
print(f'Index rebuilt: {total} memories indexed')
" || return 1
            else
                cat .loki/memory/index.json 2>/dev/null | python3 -m json.tool 2>/dev/null || echo "No index found"
            fi
            ;;

        timeline)
            # Show timeline layer
            cat .loki/memory/timeline.json 2>/dev/null | python3 -m json.tool 2>/dev/null || echo "No timeline found"
            ;;

        consolidate)
            # Run consolidation pipeline
            local hours="${2:-24}"
            LOKI_HOURS="$hours" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
import os
try:
    from memory.consolidation import ConsolidationPipeline
    from memory.storage import MemoryStorage
    storage = MemoryStorage('.loki/memory')
    pipeline = ConsolidationPipeline(storage)
    result = pipeline.consolidate(since_hours=int(os.environ.get('LOKI_HOURS', '24')))
    print('Consolidation complete:')
    print(f'  Patterns created: {result.patterns_created}')
    print(f'  Patterns merged: {result.patterns_merged}')
    print(f'  Anti-patterns: {result.anti_patterns_created}')
    print(f'  Links created: {result.links_created}')
    print(f'  Episodes processed: {result.episodes_processed}')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
            ;;

        economics)
            # v7.6.6 B-3d fix: previously only read .loki/memory/token_economics.json
            # which is rarely written by current sessions, so users saw
            # "No token economics data" while `loki kpis` correctly read
            # .loki/metrics/efficiency/iter-*.json. Unify: prefer the
            # canonical kpis source; fall back to the legacy file for
            # backward compat with pre-v7.6.6 sessions.
            # v7.7.7 fix: also check iteration-*.json (the canonical filename)
            # in addition to iter-*.json. Use a portable glob check (no compgen)
            # so the function works in any POSIX shell, not just bash.
            local _eff_dir=".loki/metrics/efficiency"
            local _legacy_file=".loki/memory/token_economics.json"
            local _have_iters=0
            if [ -d "$_eff_dir" ]; then
                if ls "$_eff_dir"/iteration-*.json >/dev/null 2>&1 || ls "$_eff_dir"/iter-*.json >/dev/null 2>&1; then
                    _have_iters=1
                fi
            fi
            if [ "$_have_iters" -eq 1 ]; then
                PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 - <<'PYEOF' 2>/dev/null
import json, glob, os, sys
# v7.7.7 fix: canonical filename is `iteration-N.json`, NOT `iter-N.json`.
# We glob both for compat with any older runs.
files = sorted(set(glob.glob('.loki/metrics/efficiency/iteration-*.json') + glob.glob('.loki/metrics/efficiency/iter-*.json')))
totals = {
    'source': '.loki/metrics/efficiency/iteration-*.json',
    'iterations': len(files),
    'total_input_tokens': 0,
    'total_output_tokens': 0,
    # Cache tiers. These DOMINATE real traffic -- a measured iteration carried
    # 797,496 cache-read tokens against 10,272 of plain input, so a report that
    # omits them describes 1% of the run and hides the single biggest lever a
    # user has on cost.
    'total_cache_read_tokens': 0,
    'total_cache_creation_tokens': 0,
    'cache_hit_ratio': 0.0,
    'total_tokens': 0,
    'total_cost_usd': 0.0,
    'total_duration_ms': 0,
    'by_model': {},
    'by_phase': {},
}
def _num(x, cast=float, default=0):
    try:
        return cast(x) if x is not None else default
    except (TypeError, ValueError):
        return default
for f in files:
    try:
        d = json.load(open(f))
    except Exception:
        continue
    ti = int(_num(d.get('input_tokens'), int))
    to = int(_num(d.get('output_tokens'), int))
    co = float(_num(d.get('cost_usd'), float))
    du = int(_num(d.get('duration_ms'), int))
    cr = int(_num(d.get('cache_read_tokens'), int))
    cw = int(_num(d.get('cache_creation_tokens'), int))
    totals['total_input_tokens'] += ti
    totals['total_output_tokens'] += to
    totals['total_cache_read_tokens'] += cr
    totals['total_cache_creation_tokens'] += cw
    # total_tokens counts EVERY token the run was billed for. Leaving cache out
    # made the headline number describe a fraction of the run.
    totals['total_tokens'] += ti + to + cr + cw
    totals['total_cost_usd'] += co
    totals['total_duration_ms'] += du
    m = d.get('model') or 'unknown'
    totals['by_model'][m] = totals['by_model'].get(m, 0) + 1
    p = d.get('phase') or 'unknown'
    totals['by_phase'][p] = totals['by_phase'].get(p, 0) + 1
# Cache hit ratio against everything read IN: the number that says whether the
# prompt cache is working. Zero reads renders 0.0 rather than a divide error.
_read_in = totals['total_input_tokens'] + totals['total_cache_read_tokens']
if _read_in > 0:
    totals['cache_hit_ratio'] = round(totals['total_cache_read_tokens'] / _read_in, 4)
print(json.dumps(totals, indent=2))
PYEOF
            elif [ -f "$_legacy_file" ]; then
                # Backward compat: pre-v7.6.6 sessions wrote here.
                cat "$_legacy_file" | python3 -m json.tool 2>/dev/null || echo "Error parsing token economics JSON"
            else
                echo "No token economics data. Run a session first."
            fi
            ;;

        retrieve)
            # Test memory retrieval
            local query="${2:-}"
            if [ -z "$query" ]; then
                echo "Usage: loki memory retrieve <query>"
                exit 1
            fi
            LOKI_QUERY="$query" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
import os
try:
    from memory.retrieval import MemoryRetrieval
    from memory.storage import MemoryStorage
    query = os.environ.get('LOKI_QUERY', '')
    storage = MemoryStorage('.loki/memory')
    retriever = MemoryRetrieval(storage)
    results = retriever.retrieve_task_aware({'goal': query}, top_k=5)
    if not results:
        print('No relevant memories found')
    else:
        for i, r in enumerate(results, 1):
            # Retrieval results expose _source/_score/_weighted_score and the
            # raw stored fields (pattern/description/goal/name), not
            # source/summary/score. Read the fields that actually exist so the
            # output is not a uniform [unknown] ... (score 0.00).
            source = r.get('_source', 'unknown')
            score = r.get('_score', r.get('_weighted_score', 0))
            summary = (r.get('pattern') or r.get('description') or r.get('goal')
                       or (r.get('context') or {}).get('goal') or r.get('name')
                       or 'No summary')[:80]
            print(f'{i}. [{source}] {summary}... (score: {score:.2f})')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
            ;;

        episode)
            # Show full episode by ID
            local id="${2:-}"
            if [ -z "$id" ]; then
                echo "Usage: loki memory episode <id>"
                exit 1
            fi
            LOKI_ID="$id" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
import os
try:
    from memory.engine import MemoryEngine
    import json
    episode_id = os.environ.get('LOKI_ID', '')
    engine = MemoryEngine(base_path='.loki/memory')
    episode = engine.get_episode(episode_id)
    if episode:
        print(json.dumps(episode.to_dict(), indent=2))
    else:
        print(f'Episode not found: {episode_id}')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
            ;;

        pattern)
            # Show semantic pattern by ID or list all patterns
            local id="${2:-}"
            if [ -z "$id" ]; then
                # List all patterns
                PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
try:
    from memory.engine import MemoryEngine
    engine = MemoryEngine(base_path='.loki/memory')
    patterns = engine.find_patterns()
    if not patterns:
        print('No patterns found')
    else:
        for p in patterns[:20]:
            pattern_text = p.pattern[:60] if hasattr(p, 'pattern') else str(p)[:60]
            confidence = p.confidence if hasattr(p, 'confidence') else 0.0
            print(f'{p.id}: {pattern_text}... (conf: {confidence:.2f})')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
            else
                LOKI_ID="$id" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
import os
try:
    from memory.engine import MemoryEngine
    import json
    pattern_id = os.environ.get('LOKI_ID', '')
    engine = MemoryEngine(base_path='.loki/memory')
    pattern = engine.get_pattern(pattern_id)
    if pattern:
        print(json.dumps(pattern.to_dict(), indent=2))
    else:
        print(f'Pattern not found: {pattern_id}')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
            fi
            ;;

        skill)
            # Show procedural skill by name or list all skills
            local name="${2:-}"
            if [ -z "$name" ]; then
                # List all skills
                if ls -1 .loki/memory/skills/*.json 2>/dev/null | head -1 >/dev/null 2>&1; then
                    ls -1 .loki/memory/skills/*.json 2>/dev/null | xargs -I{} basename {} .json
                else
                    echo "No skills found"
                fi
            else
                if [ -f ".loki/memory/skills/${name}.json" ]; then
                    cat ".loki/memory/skills/${name}.json" | python3 -m json.tool 2>/dev/null || echo "Error parsing skill JSON"
                else
                    echo "Skill not found: $name"
                fi
            fi
            ;;

        vectors)
            local vec_cmd="${2:-stats}"
            case "$vec_cmd" in
                setup)
                    echo -e "${BOLD}Vector Search Setup${NC}"
                    echo ""

                    # Detect best Python for ML packages (3.12 preferred, 3.14+ has compat issues)
                    local py_ml="python3"
                    local pip_ml="pip3"
                    if command -v /opt/homebrew/bin/python3.12 &>/dev/null; then
                        py_ml="/opt/homebrew/bin/python3.12"
                        pip_ml="/opt/homebrew/bin/pip3.12"
                        echo -e "  Using Python 3.12 for ML packages (${DIM}system python3 may be 3.14+${NC})"
                    elif command -v python3.12 &>/dev/null; then
                        py_ml="python3.12"
                        pip_ml="pip3.12"
                        echo -e "  Using Python 3.12 for ML packages"
                    else
                        local py_ver
                        py_ver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "0.0")
                        local py_minor
                        py_minor=$(echo "$py_ver" | cut -d. -f2)
                        if [ "${py_minor:-0}" -ge 13 ] 2>/dev/null; then
                            echo -e "  ${YELLOW}WARNING: Python $py_ver detected. sentence-transformers may not install.${NC}"
                            echo -e "  ${YELLOW}Install Python 3.12 for best compatibility: brew install python@3.12${NC}"
                        fi
                    fi

                    # Check/install numpy
                    echo -n "  Checking numpy... "
                    if $py_ml -c "import numpy" 2>/dev/null; then
                        echo -e "${GREEN}installed${NC}"
                    else
                        echo -n "installing... "
                        if $pip_ml install numpy 2>/dev/null || $pip_ml install --break-system-packages numpy 2>/dev/null; then
                            echo -e "${GREEN}done${NC}"
                        else
                            echo -e "${RED}failed${NC}"
                            echo "    Try: $pip_ml install numpy"
                        fi
                    fi

                    # Check/install sentence-transformers
                    echo -n "  Checking sentence-transformers... "
                    if $py_ml -c "import sentence_transformers" 2>/dev/null; then
                        echo -e "${GREEN}installed${NC}"
                    else
                        echo -n "installing (this may take a while)... "
                        if $pip_ml install sentence-transformers 2>/dev/null || $pip_ml install --break-system-packages sentence-transformers 2>/dev/null; then
                            echo -e "${GREEN}done${NC}"
                        else
                            echo -e "${RED}failed${NC}"
                            echo "    Try: $pip_ml install sentence-transformers"
                        fi
                    fi

                    # Create vector directory
                    mkdir -p .loki/memory/vectors
                    echo -e "  Vector directory: ${GREEN}.loki/memory/vectors/${NC}"

                    # Build initial index if memory files exist
                    echo ""
                    echo -n "  Building vector indices... "
                    $py_ml -c "
try:
    import sys
    sys.path.insert(0, '.')
    from memory.retrieval import MemoryRetrieval
    from memory.storage import MemoryStorage
    storage = MemoryStorage('.loki/memory')
    retriever = MemoryRetrieval(storage)
    retriever.build_indices()
    retriever.save_indices()
    print('done')
except ImportError as e:
    print(f'skipped (missing: {e})')
except Exception as e:
    print(f'error: {e}')
" 2>/dev/null || echo "skipped"

                    echo ""
                    echo -e "${GREEN}Vector search setup complete${NC}"
                    echo "Run 'loki memory search <query>' to search."
                    ;;

                rebuild)
                    local py_rb="python3"
                    if command -v /opt/homebrew/bin/python3.12 &>/dev/null; then
                        py_rb="/opt/homebrew/bin/python3.12"
                    elif command -v python3.12 &>/dev/null; then
                        py_rb="python3.12"
                    fi
                    $py_rb -c "
try:
    import sys
    sys.path.insert(0, '.')
    from memory.retrieval import MemoryRetrieval
    from memory.storage import MemoryStorage
    storage = MemoryStorage('.loki/memory')
    retriever = MemoryRetrieval(storage)
    retriever.build_indices()
    retriever.save_indices()
    print('Vector indices rebuilt')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    ;;

                stats|*)
                    echo "Vector index stats:"
                    if ls -1 .loki/memory/vectors/*.npz 2>/dev/null | head -1 >/dev/null 2>&1; then
                        for f in .loki/memory/vectors/*.npz; do
                            if [ -f "$f" ]; then
                                count=$(_NPZ_FILE="$f" python3 -c "import numpy as np, os; d=np.load(os.environ['_NPZ_FILE']); print(len(d['ids']))" 2>/dev/null || echo "error")
                                echo "  $(basename "$f"): $count vectors"
                            fi
                        done
                    else
                        echo "  No vector indices found"
                        echo "  Run 'loki memory vectors setup' to initialize"
                    fi
                    ;;
            esac
            ;;

        namespace|ns)
            # Namespace management (COMP-005)
            local ns_cmd="${2:-detect}"
            shift 2 2>/dev/null || true

            case "$ns_cmd" in
                detect)
                    # Auto-detect namespace from current directory
                    python3 -c "
try:
    from memory.namespace import detect_namespace
    ns = detect_namespace()
    print(f'Detected namespace: {ns}')
except ImportError as e:
    print(f'Error: memory.namespace module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    ;;

                list|ls)
                    # List all namespaces
                    python3 -c "
try:
    from memory.storage import MemoryStorage
    storage = MemoryStorage('.loki/memory')
    namespaces = storage.list_namespaces()
    if not namespaces:
        print('No namespaces found')
    else:
        print('Available namespaces:')
        for ns in namespaces:
            stats = storage.get_namespace_stats(ns)
            print(f'  {ns}: {stats[\"episode_count\"]} episodes, {stats[\"pattern_count\"]} patterns, {stats[\"skill_count\"]} skills')
except ImportError as e:
    print(f'Error: memory.storage module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    ;;

                create)
                    # Create a new namespace
                    local name="${1:-}"
                    local parent="${2:-}"

                    if [ -z "$name" ]; then
                        echo "Usage: loki memory namespace create <name> [parent]"
                        exit 1
                    fi

                    LOKI_NS_NAME="$name" LOKI_NS_PARENT="$parent" python3 -c "
import os
try:
    from memory.namespace import NamespaceManager
    name = os.environ.get('LOKI_NS_NAME', '')
    parent = os.environ.get('LOKI_NS_PARENT', '') or None

    manager = NamespaceManager('.loki/memory')
    info = manager.create_namespace(name=name, parent=parent)
    print(f'Created namespace: {info.name}')
    print(f'  Path: {info.path}')
    if info.parent:
        print(f'  Parent: {info.parent}')
except ValueError as e:
    print(f'Error: {e}')
except ImportError as e:
    print(f'Error: memory.namespace module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    ;;

                delete|rm)
                    # Delete a namespace
                    local name="${1:-}"
                    local delete_data="${2:-}"

                    if [ -z "$name" ]; then
                        echo "Usage: loki memory namespace delete <name> [--data]"
                        exit 1
                    fi

                    local with_data="false"
                    if [ "$delete_data" = "--data" ]; then
                        with_data="true"
                    fi

                    LOKI_NS_NAME="$name" LOKI_DELETE_DATA="$with_data" python3 -c "
import os
try:
    from memory.namespace import NamespaceManager
    name = os.environ.get('LOKI_NS_NAME', '')
    delete_data = os.environ.get('LOKI_DELETE_DATA', 'false') == 'true'

    manager = NamespaceManager('.loki/memory')
    result = manager.delete_namespace(name, delete_data=delete_data)
    if result:
        msg = f'Deleted namespace: {name}'
        if delete_data:
            msg += ' (including data)'
        print(msg)
    else:
        print(f'Namespace not found: {name}')
except ValueError as e:
    print(f'Error: {e}')
except ImportError as e:
    print(f'Error: memory.namespace module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    ;;

                stats)
                    # Show stats for a namespace
                    local name="${1:-}"

                    if [ -z "$name" ]; then
                        # Show current namespace stats
                        python3 -c "
try:
    from memory.namespace import detect_namespace
    from memory.storage import MemoryStorage
    ns = detect_namespace()
    storage = MemoryStorage('.loki/memory', namespace=ns)
    stats = storage.get_namespace_stats()
    print(f'Namespace: {stats[\"namespace\"]}')
    print(f'  Path: {stats[\"path\"]}')
    print(f'  Episodes: {stats[\"episode_count\"]}')
    print(f'  Patterns: {stats[\"pattern_count\"]}')
    print(f'  Skills: {stats[\"skill_count\"]}')
    print(f'  Total: {stats[\"total_count\"]}')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    else
                        LOKI_NS_NAME="$name" python3 -c "
import os
try:
    from memory.storage import MemoryStorage
    name = os.environ.get('LOKI_NS_NAME', '')
    storage = MemoryStorage('.loki/memory', namespace=name)
    stats = storage.get_namespace_stats()
    print(f'Namespace: {stats[\"namespace\"]}')
    print(f'  Path: {stats[\"path\"]}')
    print(f'  Episodes: {stats[\"episode_count\"]}')
    print(f'  Patterns: {stats[\"pattern_count\"]}')
    print(f'  Skills: {stats[\"skill_count\"]}')
    print(f'  Total: {stats[\"total_count\"]}')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    fi
                    ;;

                copy)
                    # Copy from current namespace to target
                    local target="${1:-}"

                    if [ -z "$target" ]; then
                        echo "Usage: loki memory namespace copy <target-namespace>"
                        exit 1
                    fi

                    LOKI_NS_TARGET="$target" python3 -c "
import os
try:
    from memory.namespace import detect_namespace
    from memory.storage import MemoryStorage
    target = os.environ.get('LOKI_NS_TARGET', '')

    source_ns = detect_namespace()
    storage = MemoryStorage('.loki/memory', namespace=source_ns)
    result = storage.copy_to_namespace(target)

    print(f'Copied from {source_ns} to {target}:')
    print(f'  Episodes: {result[\"episodes\"]}')
    print(f'  Patterns: {result[\"patterns\"]}')
    print(f'  Skills: {result[\"skills\"]}')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    ;;

                merge)
                    # Merge from source namespace into current
                    local source="${1:-}"

                    if [ -z "$source" ]; then
                        echo "Usage: loki memory namespace merge <source-namespace>"
                        exit 1
                    fi

                    LOKI_NS_SOURCE="$source" python3 -c "
import os
try:
    from memory.namespace import detect_namespace
    from memory.storage import MemoryStorage
    source = os.environ.get('LOKI_NS_SOURCE', '')

    target_ns = detect_namespace()
    storage = MemoryStorage('.loki/memory', namespace=target_ns)
    result = storage.merge_from_namespace(source, deduplicate=True)

    print(f'Merged from {source} into {target_ns}:')
    print(f'  Episodes: {result[\"episodes\"]}')
    print(f'  Patterns: {result[\"patterns\"]}')
    print(f'  Skills: {result[\"skills\"]}')
except ImportError as e:
    print(f'Error: Required module not found - {e}')
except Exception as e:
    print(f'Error: {e}')
" 2>/dev/null
                    ;;

                help|--help|-h)
                    echo "Namespace management commands:"
                    echo ""
                    echo "  detect            Auto-detect namespace from current directory"
                    echo "  list              List all available namespaces"
                    echo "  create <name>     Create a new namespace"
                    echo "  delete <name>     Delete a namespace (--data to also delete data)"
                    echo "  stats [name]      Show stats for a namespace"
                    echo "  copy <target>     Copy current namespace to target"
                    echo "  merge <source>    Merge source namespace into current"
                    echo ""
                    echo "Examples:"
                    echo "  loki memory namespace detect"
                    echo "  loki memory namespace list"
                    echo "  loki memory namespace create my-project"
                    echo "  loki memory namespace create child-project parent-project"
                    echo "  loki memory namespace stats my-project"
                    echo "  loki memory namespace copy backup-ns"
                    echo "  loki memory namespace merge shared-patterns"
                    ;;

                *)
                    echo -e "${RED}Unknown namespace command: $ns_cmd${NC}"
                    echo "Run 'loki memory namespace help' for usage."
                    exit 1
                    ;;
            esac
            ;;

        --namespace|-n)
            # Global option to specify namespace for memory commands
            echo "Note: --namespace is a global option, use it before the subcommand:"
            echo "  loki memory --namespace=myproject retrieve 'query'"
            echo ""
            echo "Or use the namespace subcommand:"
            echo "  loki memory namespace stats myproject"
            ;;

        --help|-h|help)
            echo -e "${BOLD}loki memory${NC} - Manage cross-project learnings and memory system"
            echo ""
            echo "Usage: loki memory <command> [args]"
            echo ""
            echo "Learnings Commands:"
            echo "  list [--json]     Show summary of all learnings"
            echo "  show [type]       Show entries (patterns|mistakes|successes|all)"
            echo "                    Options: --limit N, --project P"
            echo "  search <query>    Search across all learnings"
            echo "  stats             Show statistics by project and category"
            echo "  export [file]     Export to JSON file"
            echo "  dedupe            Remove duplicate entries"
            echo "  clear [type]      Clear learnings (all or specific type)"
            echo "  compound [cmd]    Compound-solutions knowledge store (was: loki compound)"
            echo "  lessons [--json]  PR review lessons with their uses and outcomes"
            echo "  learn <o/r#PR>    Learn lessons from a merged PR's review comments and human commits"
            echo "  promote <prl-id>  Share one lesson with every repo's briefs (undo with forget)"
            echo "  forget <prl-id>   Delete one PR lesson and print its text"
            echo "  brief [--task T]  Print the lessons and learnings block injected into a run"
            echo ""
            echo "Memory System Commands:"
            echo "  index [rebuild]   Show or rebuild the memory index layer"
            echo "  timeline          Show timeline layer with temporal context"
            echo "  consolidate [h]   Run consolidation pipeline (default: 24 hours)"
            echo "  economics         Show token economics data"
            echo "  retrieve <query>  Test memory retrieval for a query"
            echo "  episode <id>      Show full episode by ID"
            echo "  pattern [id]      Show pattern by ID or list all patterns"
            echo "  skill [name]      Show skill by name or list all skills"
            echo "  vectors [rebuild] Show vector index stats or rebuild indices"
            echo ""
            echo "Namespace Commands:"
            echo "  namespace detect       Auto-detect namespace from current directory"
            echo "  namespace list         List all available namespaces"
            echo "  namespace create NAME  Create a new namespace"
            echo "  namespace delete NAME  Delete a namespace"
            echo "  namespace stats [NAME] Show stats for a namespace"
            echo "  namespace copy TARGET  Copy current namespace to target"
            echo "  namespace merge SOURCE Merge source namespace into current"
            echo ""
            echo "Examples:"
            echo "  loki memory list"
            echo "  loki memory show mistakes --limit 10"
            echo "  loki memory show patterns --project myapp"
            echo "  loki memory search 'rate limit'"
            echo "  loki memory dedupe"
            echo "  loki memory export backup.json"
            echo ""
            echo "  loki memory index"
            echo "  loki memory index rebuild"
            echo "  loki memory consolidate 48"
            echo "  loki memory retrieve 'API rate limiting'"
            echo "  loki memory episode ep-20260202-abc123"
            echo "  loki memory pattern"
            echo "  loki memory pattern pat-api-error-handling"
            echo "  loki memory skill"
            echo "  loki memory vectors"
            echo "  loki memory vectors rebuild"
            echo ""
            echo "  loki memory ingest --from-claude-transcript <path>"
            echo "  loki memory ingest --from-stdin"
            echo "  loki memory replay <episode-id> [--json]"
            echo "  loki memory crossproject --for 'build api'"
            echo "  loki memory graph --export graph.json"
            echo "  loki memory graph rebuild"
            echo "  loki memory enable-hook"
            echo "  loki memory disable-hook"
            echo ""
            echo "  loki memory namespace detect"
            echo "  loki memory namespace list"
            echo "  loki memory namespace create my-project"
            echo "  loki memory namespace stats"
            ;;

        ingest)
            # v7.7.18 capture wedge: ingest a Claude Code session transcript
            # into the project's .loki/memory/ store. Solves the diagnosis
            # root cause where memory only captured during `loki start`.
            #
            # Usage:
            #   loki memory ingest --from-claude-transcript <path>
            #   loki memory ingest --from-stdin   (reads a JSON summary doc)
            shift  # drop "ingest"
            local _ingest_mode=""
            local _ingest_path=""
            while [ $# -gt 0 ]; do
                case "$1" in
                    --from-claude-transcript)
                        _ingest_mode="transcript"
                        _ingest_path="${2:-}"
                        if [ $# -ge 2 ]; then shift 2; else shift; fi
                        ;;
                    --from-stdin)
                        _ingest_mode="stdin"
                        shift
                        ;;
                    -h|--help)
                        echo "Usage: loki memory ingest --from-claude-transcript <path>"
                        echo "       loki memory ingest --from-stdin   (JSON doc on stdin)"
                        echo ""
                        echo "Honors LOKI_MEMORY_CAPTURE_DISABLED=true (escape hatch)."
                        return 0
                        ;;
                    *) shift ;;
                esac
            done
            if [ -z "$_ingest_mode" ]; then
                echo -e "${RED}Usage: loki memory ingest --from-claude-transcript <path> | --from-stdin${NC}"
                exit 1
            fi
            local _project_root_for_ingest="${SKILL_DIR:-$(pwd)}"
            local _target_memory_dir
            _target_memory_dir="$(pwd)/.loki/memory"
            mkdir -p "$_target_memory_dir" 2>/dev/null || true
            if [ "$_ingest_mode" = "transcript" ]; then
                if [ -z "$_ingest_path" ] || [ ! -f "$_ingest_path" ]; then
                    echo -e "${RED}Transcript not found: $_ingest_path${NC}"
                    exit 1
                fi
                PYTHONPATH="$_project_root_for_ingest" \
                    python3 -c "
import sys, json
from memory.ingest import ingest_from_claude_transcript
path = ingest_from_claude_transcript(sys.argv[1], sys.argv[2])
print(json.dumps({'episode_path': path}))
" "$_ingest_path" "$_target_memory_dir"
            else
                PYTHONPATH="$_project_root_for_ingest" \
                    python3 -c "
import sys, json
from memory.ingest import ingest_from_summary
doc = json.loads(sys.stdin.read())
path = ingest_from_summary(
    sys.argv[1],
    goal=doc.get('goal',''),
    outcome=doc.get('outcome','success'),
    files_modified=doc.get('files_modified',[]),
    files_read=doc.get('files_read',[]),
    tool_calls_summary=doc.get('tool_calls_summary',''),
    duration_seconds=int(doc.get('duration_seconds',0) or 0),
)
print(json.dumps({'episode_path': path}))
" "$_target_memory_dir"
            fi
            ;;

        replay)
            # v7.7.22 wow feature: READ-ONLY deterministic session replay.
            # Renders a past episode's action timeline + current state of
            # touched files. Does NOT re-execute (LLM tool_uses are
            # non-deterministic + re-running edits could clobber work;
            # --apply deferred to a future release).
            # Usage: loki memory replay <episode-id> [--json]
            shift  # drop "replay"
            local _replay_id=""
            local _replay_json="false"
            while [ $# -gt 0 ]; do
                case "$1" in
                    --json) _replay_json="true"; shift ;;
                    -h|--help) echo "Usage: loki memory replay <episode-id> [--json]"; return 0 ;;
                    *) [ -z "$_replay_id" ] && _replay_id="$1"; shift ;;
                esac
            done
            if [ -z "$_replay_id" ]; then
                echo -e "${RED}Usage: loki memory replay <episode-id> [--json]${NC}"
                exit 1
            fi
            local _replay_mem
            _replay_mem="$(pwd)/.loki/memory"
            PYTHONPATH="${SKILL_DIR:-$(pwd)}" _LOKI_REPLAY_JSON="$_replay_json" python3 -c "
import sys, os, json
from memory.replay import replay_episode, render_markdown
report = replay_episode(sys.argv[1], sys.argv[2])
if os.environ.get('_LOKI_REPLAY_JSON') == 'true':
    print(json.dumps(report, indent=2, default=str))
else:
    print(render_markdown(report))
sys.exit(0 if report.get('found') else 1)
" "$_replay_id" "$_replay_mem"
            ;;

        crossproject)
            # v7.7.20: surface cross-project knowledge-graph patterns
            # (wakes the previously-dead memory/knowledge_graph.py).
            # Usage: loki memory crossproject [--for <goal text>]
            shift  # drop "crossproject"
            local _cp_query="general"
            while [ $# -gt 0 ]; do
                case "$1" in
                    --for) _cp_query="${2:-general}"; if [ $# -ge 2 ]; then shift 2; else shift; fi ;;
                    -h|--help) echo "Usage: loki memory crossproject [--for <goal>]"; return 0 ;;
                    *) shift ;;
                esac
            done
            PYTHONPATH="${SKILL_DIR:-$(pwd)}" python3 -c "
import sys, json
try:
    from memory.knowledge_graph import OrganizationKnowledgeGraph
    kg = OrganizationKnowledgeGraph()
    patterns = kg.query_patterns(sys.argv[1], max_results=10)
    if not patterns:
        print('No cross-project patterns found. Run sessions in multiple projects to build the graph.')
    else:
        for i, p in enumerate(patterns, 1):
            desc = p.get('description', p.get('pattern', p)) if isinstance(p, dict) else p
            print(f'{i}. {str(desc)[:160]}')
except ImportError:
    print('knowledge_graph module not available')
except Exception as e:
    print(f'Error: {e}')
" "$_cp_query"
            ;;

        graph)
            # v7.7.20: export OR rebuild the cross-project knowledge graph.
            # Usage: loki memory graph [--export <path>]
            #        loki memory graph rebuild   # populate from semantic patterns
            shift  # drop "graph"
            # v7.7.20 council fix (Opus 1): the read side (query_patterns)
            # was woken but the WRITE side had no caller, so the graph
            # stayed empty forever. `graph rebuild` mines .loki/memory/
            # semantic/*.json across discovered projects and persists them
            # to the org knowledge graph -- the population path that makes
            # crossproject + load_memory_context augmentation non-inert.
            if [ "${1:-}" = "rebuild" ]; then
                shift
                PYTHONPATH="${SKILL_DIR:-$(pwd)}" python3 -c "
import sys, os
try:
    from memory.knowledge_graph import OrganizationKnowledgeGraph
    from memory.cross_project import CrossProjectIndex
    cpi = CrossProjectIndex()
    cpi.discover_projects()
    project_dirs = cpi.get_project_dirs()
    kg = OrganizationKnowledgeGraph()
    # v7.7.20 council fix (Opus 1): save_patterns appends, so a naive
    # rebuild would accumulate duplicates across runs. Dedup the UNION of
    # existing + freshly-extracted, then truncate-rewrite so rebuild is
    # idempotent.
    existing = kg.load_patterns(limit=100000)
    fresh = kg.extract_patterns(project_dirs)
    merged = kg.deduplicate_patterns(list(existing) + list(fresh))
    # Truncate then write the deduped union (save_patterns appends).
    try:
        if os.path.exists(kg.patterns_file):
            open(kg.patterns_file, 'w').close()
    except OSError:
        pass
    if merged:
        kg.save_patterns(merged)
        print(f'Rebuilt knowledge graph: {len(merged)} unique patterns from {len(project_dirs)} project(s)')
    else:
        print(f'No semantic patterns found across {len(project_dirs)} project(s). Run sessions + consolidation first.')
except ImportError as e:
    print(f'knowledge_graph/cross_project module not available: {e}')
except Exception as e:
    print(f'Error: {e}')
"
                return 0
            fi
            local _graph_export=""
            while [ $# -gt 0 ]; do
                case "$1" in
                    --export) _graph_export="${2:-}"; if [ $# -ge 2 ]; then shift 2; else shift; fi ;;
                    -h|--help) echo "Usage: loki memory graph [--export <path>] | loki memory graph rebuild"; return 0 ;;
                    *) shift ;;
                esac
            done
            PYTHONPATH="${SKILL_DIR:-$(pwd)}" _LOKI_GRAPH_EXPORT="$_graph_export" python3 -c "
import sys, os, json
export_path = os.environ.get('_LOKI_GRAPH_EXPORT', '')
try:
    from memory.knowledge_graph import OrganizationKnowledgeGraph
    kg = OrganizationKnowledgeGraph()
    patterns = kg.load_patterns(limit=100)
    summary = {'pattern_count': len(patterns), 'patterns': patterns[:100]}
    if export_path:
        with open(export_path, 'w') as f:
            json.dump(summary, f, indent=2, default=str)
        print(f'Exported {len(patterns)} patterns to {export_path}')
    else:
        print(json.dumps(summary, indent=2, default=str)[:4000])
except ImportError:
    print('knowledge_graph module not available')
except Exception as e:
    print(f'Error: {e}')
"
            ;;

        enable-hook)
            # v7.7.20: idempotently install a Claude Code SessionEnd hook
            # using the VERIFIED schema {matcher, hooks:[{type,command}]}.
            # Per WebSearch (v7.7.18 council): SessionEnd fires on /clear,
            # payload is JSON on stdin with transcript_path. The shipped
            # script claude/hooks/loki-session-end.sh handles both stdin
            # JSON and env-var fallback, so we point the hook at it.
            # No-op under LOKI_MEMORY_HOOK_DISABLED=true.
            shift
            if [ "${LOKI_MEMORY_HOOK_DISABLED:-}" = "true" ]; then
                echo -e "${YELLOW}Hook install skipped (LOKI_MEMORY_HOOK_DISABLED=true)${NC}"
                return 0
            fi
            local _settings="$HOME/.claude/settings.json"
            local _hook_script="${SKILL_DIR:-$(pwd)}/claude/hooks/loki-session-end.sh"
            # FAIL CLOSED on a missing script. This path was built unguarded and
            # written into the user's REAL ~/.claude/settings.json, then reported
            # green "installed" -- while claude/ was absent from package.json
            # files[], so for every npm user the hook pointed at a file that did
            # not exist. The install claimed success and transcripts were never
            # ingested; the SessionEnd failure surfaces nowhere the user reads.
            # Writing a settings entry we cannot honor is worse than not writing
            # one, so refuse and say why instead of mutating their config.
            if [ ! -f "$_hook_script" ]; then
                echo -e "${RED}Cannot install the SessionEnd hook: script not found${NC}" >&2
                echo -e "${YELLOW}  expected: $_hook_script${NC}" >&2
                echo -e "${YELLOW}  ~/.claude/settings.json was NOT modified.${NC}" >&2
                return 1
            fi
            mkdir -p "$(dirname "$_settings")" 2>/dev/null || true
            [ ! -f "$_settings" ] && echo '{}' > "$_settings"
            _LOKI_HOOK_SCRIPT="$_hook_script" _LOKI_SETTINGS="$_settings" python3 -c "
import json, os, sys, tempfile
settings_path = os.environ['_LOKI_SETTINGS']
hook_script = os.environ['_LOKI_HOOK_SCRIPT']
hook_cmd = f'bash {hook_script}'
try:
    with open(settings_path) as f:
        data = json.load(f)
except Exception:
    data = {}
hooks = data.setdefault('hooks', {})
session_end = hooks.setdefault('SessionEnd', [])
# Verified Claude Code schema: list of {matcher, hooks:[{type,command}]}.
# Idempotency: detect an existing entry whose nested command references
# our hook script.
for entry in session_end:
    if isinstance(entry, dict):
        for h in entry.get('hooks', []):
            if isinstance(h, dict) and 'loki-session-end.sh' in str(h.get('command', '')):
                print('already-installed')
                sys.exit(0)
session_end.append({
    'matcher': 'clear',
    'hooks': [{'type': 'command', 'command': hook_cmd}],
})
tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(settings_path), prefix='.loki-settings-')
with os.fdopen(tmp_fd, 'w') as f:
    json.dump(data, f, indent=2)
os.replace(tmp_path, settings_path)
print('installed')
"
            local _r=$?
            if [ $_r -eq 0 ]; then
                echo -e "${GREEN}SessionEnd hook ready.${NC} Fires on /clear; pipes transcript to loki memory ingest."
                echo -e "${CYAN}Note: SessionEnd only fires on /clear, not normal exits (Claude Code limitation).${NC}"
                echo -e "${CYAN}Remove with: loki memory disable-hook${NC}"
            fi
            ;;

        disable-hook)
            # v7.7.20: reverse counterpart. Removes any SessionEnd entry
            # whose nested command references loki-session-end.sh.
            shift
            local _settings="$HOME/.claude/settings.json"
            if [ ! -f "$_settings" ]; then
                echo -e "${YELLOW}No ~/.claude/settings.json to clean${NC}"
                return 0
            fi
            _LOKI_SETTINGS="$_settings" python3 -c "
import json, os, sys, tempfile
settings_path = os.environ['_LOKI_SETTINGS']
try:
    with open(settings_path) as f:
        data = json.load(f)
except Exception:
    print('settings-not-readable')
    sys.exit(0)
hooks = data.get('hooks', {})
session_end = hooks.get('SessionEnd', [])
def refs_loki(entry):
    if not isinstance(entry, dict):
        return False
    for h in entry.get('hooks', []):
        if isinstance(h, dict) and 'loki-session-end.sh' in str(h.get('command', '')):
            return True
    return False
filtered = [e for e in session_end if not refs_loki(e)]
if len(filtered) == len(session_end):
    print('not-installed')
    sys.exit(0)
hooks['SessionEnd'] = filtered
data['hooks'] = hooks
tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(settings_path), prefix='.loki-settings-')
with os.fdopen(tmp_fd, 'w') as f:
    json.dump(data, f, indent=2)
os.replace(tmp_path, settings_path)
print('removed')
"
            ;;

        compound)
            # CLI consolidation (Phase B, design item 7): 'compound' is folded
            # under the 'memory' noun as 'memory compound'. This arm forwards to
            # the existing cmd_compound handler unchanged (no handler logic
            # moves). The top-level 'compound' token stays a deprecated
            # forwarding alias for back-compat. cmd_memory does not shift before
            # the case, so $@ here is still 'compound <args>'; drop the leading
            # 'compound' token, then forward the rest verbatim.
            shift 2>/dev/null || true
            cmd_compound "$@"
            ;;

        *)
            # Error-channel parity with the consolidated noun family (analyze,
            # modernize): unknown-subcommand diagnostics go to STDERR so a scripted
            # consumer capturing stdout is not handed error text. Exit 1 unchanged.
            echo -e "${RED}Unknown memory command: $subcommand${NC}" >&2
            echo "Run 'loki memory help' for usage." >&2
            exit 1
            ;;
    esac
}

# Knowledge Compounding - Structured Solutions (v5.30.0)
# Inspired by Compound Engineering Plugin's docs/solutions/ with YAML frontmatter
cmd_compound() {
    local subcommand="${1:-help}"
    shift 2>/dev/null || true

    local solutions_dir="${HOME}/.loki/solutions"

    case "$subcommand" in
        list|ls)
            echo -e "${BOLD}Compound Solutions${NC}"
            echo ""

            if [ ! -d "$solutions_dir" ]; then
                echo "  No solutions yet. Solutions are created automatically during"
                echo "  Loki sessions or manually with 'loki compound run'."
                echo ""
                echo "  Location: $solutions_dir"
                return
            fi

            local total=0
            for category in security performance architecture testing debugging deployment general; do
                local cat_dir="$solutions_dir/$category"
                if [ -d "$cat_dir" ]; then
                    local count=$(find "$cat_dir" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
                    if [ "$count" -gt 0 ]; then
                        printf "  %-16s ${GREEN}%d${NC} solutions\n" "$category" "$count"
                        total=$((total + count))
                    fi
                fi
            done

            if [ "$total" -eq 0 ]; then
                echo "  No solutions yet."
            else
                echo ""
                echo "  Total: ${total} solutions"
            fi
            echo ""
            echo "  Location: $solutions_dir"
            echo ""
            echo "  Use 'loki compound show <category>' to view solutions"
            ;;

        show)
            local category="${1:-}"
            if [ -z "$category" ]; then
                echo -e "${RED}Error: Specify a category${NC}"
                echo "Categories: security, performance, architecture, testing, debugging, deployment, general"
                return 1
            fi

            local cat_dir="$solutions_dir/$category"
            if [ ! -d "$cat_dir" ]; then
                echo "No solutions in category: $category"
                return
            fi

            echo -e "${BOLD}Solutions: $category${NC}"
            echo ""

            for file in "$cat_dir"/*.md; do
                [ -f "$file" ] || continue
                # Extract title from YAML frontmatter
                local title=$(grep '^title:' "$file" 2>/dev/null | head -1 | sed 's/title: *"//;s/"$//')
                local confidence=$(grep '^confidence:' "$file" 2>/dev/null | head -1 | sed 's/confidence: *//')
                local project=$(grep '^source_project:' "$file" 2>/dev/null | head -1 | sed 's/source_project: *"//;s/"$//')
                echo -e "  ${GREEN}*${NC} ${title:-$(basename "$file" .md)}"
                [ -n "$confidence" ] && echo -e "    confidence: ${confidence}  project: ${project:-unknown}"
            done
            echo ""
            ;;

        search)
            local query="${1:-}"
            if [ -z "$query" ]; then
                echo -e "${RED}Error: Specify a search query${NC}"
                echo "Usage: loki compound search <query>"
                return 1
            fi

            echo -e "${BOLD}Searching solutions for: ${query}${NC}"
            echo ""

            if [ ! -d "$solutions_dir" ]; then
                echo "No solutions directory found."
                return
            fi

            local found=0
            while IFS= read -r file; do
                local title=$(grep '^title:' "$file" 2>/dev/null | head -1 | sed 's/title: *"//;s/"$//')
                local category=$(grep '^category:' "$file" 2>/dev/null | head -1 | sed 's/category: *//')
                echo -e "  [${CYAN}${category}${NC}] ${title:-$(basename "$file" .md)}"
                echo -e "    ${DIM}${file}${NC}"
                found=$((found + 1))
            done < <(grep -rl "$query" "$solutions_dir" 2>/dev/null || true)

            if [ "$found" -eq 0 ]; then
                echo "  No solutions matching: $query"
            else
                echo ""
                echo "  Found: ${found} solutions"
            fi
            ;;

        run)
            echo -e "${BOLD}Compounding learnings into solutions...${NC}"
            echo ""

            local learnings_dir="${HOME}/.loki/learnings"
            if [ ! -d "$learnings_dir" ]; then
                echo "  No learnings directory found at: $learnings_dir"
                echo "  Run a Loki session first to generate learnings."
                return
            fi

            python3 << 'COMPOUND_RUN_SCRIPT'
import json, os, re
from datetime import datetime, timezone
from collections import defaultdict
learnings_dir = os.path.expanduser("~/.loki/learnings")
solutions_dir = os.path.expanduser("~/.loki/solutions")
CATEGORIES = ["security", "performance", "architecture", "testing", "debugging", "deployment", "general"]
CATEGORY_KEYWORDS = {
    "security": ["auth", "login", "password", "token", "injection", "xss", "csrf", "cors", "secret", "encrypt", "permission"],
    "performance": ["cache", "query", "n+1", "memory", "leak", "slow", "timeout", "pool", "index", "optimize", "bundle"],
    "architecture": ["pattern", "solid", "coupling", "abstraction", "module", "interface", "design", "refactor", "structure"],
    "testing": ["test", "mock", "fixture", "coverage", "assert", "spec", "e2e", "playwright", "jest", "flaky"],
    "debugging": ["debug", "error", "trace", "log", "stack", "crash", "exception", "breakpoint", "inspect"],
    "deployment": ["deploy", "docker", "ci", "cd", "pipeline", "kubernetes", "nginx", "ssl", "domain", "env", "config"],
}
def load_jsonl(filepath):
    entries = []
    if not os.path.exists(filepath): return entries
    with open(filepath, 'r') as f:
        for line in f:
            try:
                entry = json.loads(line)
                if 'description' in entry: entries.append(entry)
            except: continue
    return entries
def classify_category(description):
    desc_lower = description.lower()
    scores = {cat: sum(1 for kw in kws if kw in desc_lower) for cat, kws in CATEGORY_KEYWORDS.items()}
    best = max(scores, key=scores.get)
    return best if scores[best] > 0 else "general"
def slugify(text):
    return re.sub(r'[^a-z0-9]+', '-', text.lower().strip()).strip('-')[:80]
def solution_exists(solutions_dir, title_slug):
    for cat in CATEGORIES:
        cat_dir = os.path.join(solutions_dir, cat)
        if os.path.exists(cat_dir) and os.path.exists(os.path.join(cat_dir, f"{title_slug}.md")):
            return True
    return False
patterns = load_jsonl(os.path.join(learnings_dir, "patterns.jsonl"))
mistakes = load_jsonl(os.path.join(learnings_dir, "mistakes.jsonl"))
successes = load_jsonl(os.path.join(learnings_dir, "successes.jsonl"))
print(f"  Loaded: {len(patterns)} patterns, {len(mistakes)} mistakes, {len(successes)} successes")
grouped = defaultdict(list)
for entry in patterns + mistakes + successes:
    grouped[classify_category(entry.get('description', ''))].append(entry)
created = 0
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
for category, entries in grouped.items():
    if len(entries) < 2: continue
    cat_dir = os.path.join(solutions_dir, category)
    os.makedirs(cat_dir, exist_ok=True)
    best_entry = max(entries, key=lambda e: len(e.get('description', '')))
    title = best_entry['description'][:120]
    slug = slugify(title)
    if solution_exists(solutions_dir, slug): continue
    all_words = ' '.join(e.get('description', '') for e in entries).lower()
    tags = []
    for kw_list in CATEGORY_KEYWORDS.values():
        for kw in kw_list:
            if kw in all_words and kw not in tags: tags.append(kw)
    tags = tags[:8]
    symptoms = [e.get('description', '')[:200] for e in entries
                if any(w in e.get('description', '').lower() for w in ['error', 'fail', 'bug', 'crash', 'issue'])][:4]
    if not symptoms: symptoms = [entries[0].get('description', '')[:200]]
    solution_lines = [f"- {e.get('description', '')}" for e in entries
                      if not any(w in e.get('description', '').lower() for w in ['error', 'fail', 'bug', 'crash'])]
    if not solution_lines: solution_lines = [f"- {entries[0].get('description', '')}"]
    project = best_entry.get('project', os.path.basename(os.getcwd()))
    filepath = os.path.join(cat_dir, f"{slug}.md")
    with open(filepath, 'w') as f:
        f.write(f"---\ntitle: \"{title}\"\ncategory: {category}\ntags: [{', '.join(tags)}]\nsymptoms:\n")
        for s in symptoms: f.write(f'  - "{s}"\n')
        f.write(f'root_cause: "Identified from {len(entries)} related learnings"\n')
        f.write(f'prevention: "See solution details below"\nconfidence: {min(0.5 + 0.1 * len(entries), 0.95):.2f}\n')
        f.write(f'source_project: "{project}"\ncreated: "{now}"\napplied_count: 0\n---\n\n')
        f.write("## Solution\n\n" + '\n'.join(solution_lines) + '\n\n')
        f.write(f"## Context\n\nCompounded from {len(entries)} learnings from project: {project}\n")
    created += 1
    print(f"  Created: {category}/{slug}.md")
if created > 0: print(f"\n  Compounded {created} new solution files to {solutions_dir}")
else: print("  No new solutions to compound (need 2+ related learnings per category)")
COMPOUND_RUN_SCRIPT
            ;;

        stats)
            echo -e "${BOLD}Compound Solution Statistics${NC}"
            echo ""

            if [ ! -d "$solutions_dir" ]; then
                echo "  No solutions directory found."
                return
            fi

            local total=0
            local newest=""
            local oldest=""

            for category in security performance architecture testing debugging deployment general; do
                local cat_dir="$solutions_dir/$category"
                if [ -d "$cat_dir" ]; then
                    local count=$(find "$cat_dir" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
                    total=$((total + count))
                fi
            done

            echo "  Total solutions: ${total}"

            if [ "$total" -gt 0 ]; then
                # Find newest and oldest
                newest=$(find "$solutions_dir" -name "*.md" -exec stat -f '%m %N' {} \; 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
                oldest=$(find "$solutions_dir" -name "*.md" -exec stat -f '%m %N' {} \; 2>/dev/null | sort -n | head -1 | cut -d' ' -f2-)

                if [ -n "$newest" ]; then
                    local newest_title=$(grep '^title:' "$newest" 2>/dev/null | head -1 | sed 's/title: *"//;s/"$//')
                    echo "  Newest: ${newest_title:-$(basename "$newest" .md)}"
                fi
                if [ -n "$oldest" ]; then
                    local oldest_title=$(grep '^title:' "$oldest" 2>/dev/null | head -1 | sed 's/title: *"//;s/"$//')
                    echo "  Oldest: ${oldest_title:-$(basename "$oldest" .md)}"
                fi
            fi

            echo ""
            echo "  Location: $solutions_dir"
            ;;

        help|--help|-h)
            echo -e "${BOLD}loki memory compound${NC} - Knowledge compounding system"
            echo ""
            echo "Extracts structured solutions from cross-project learnings."
            echo "Solutions are stored as markdown files with YAML frontmatter"
            echo "at ~/.loki/solutions/{category}/ and fed back into future planning."
            echo ""
            echo "Usage: loki memory compound <command>   (alias: loki compound)"
            echo ""
            echo "Commands:"
            echo "  list              List all solutions by category"
            echo "  show <category>   Show solutions in a category"
            echo "  search <query>    Search across all solutions"
            echo "  run               Manually compound current learnings into solutions"
            echo "  stats             Solution statistics"
            echo "  help              Show this help"
            echo ""
            echo "Categories: security, performance, architecture, testing,"
            echo "            debugging, deployment, general"
            echo ""
            echo "Solutions are created automatically at session end and"
            echo "loaded during the REASON phase for relevant tasks."
            ;;

        *)
            echo -e "${RED}Unknown compound command: $subcommand${NC}"
            echo "Run 'loki compound help' for usage."
            return 1
            ;;
    esac
}

# Checkpoint management - save and restore session state (v5.34.0)
cmd_checkpoint() {
    local subcommand="${1:-list}"
    shift 2>/dev/null || true

    local checkpoints_dir=".loki/state/checkpoints"
    local index_file="$checkpoints_dir/index.jsonl"

    case "$subcommand" in
        list|ls)
            echo -e "${BOLD}Session Checkpoints${NC}"
            echo ""

            if [ ! -f "$index_file" ]; then
                echo "  No checkpoints yet."
                echo ""
                echo "  Create one with: loki checkpoint create [message]"
                return 0
            fi

            local count=0
            local lines=()
            while IFS= read -r line; do
                lines+=("$line")
            done < "$index_file"

            # Show last 10 entries (most recent last)
            local total=${#lines[@]}
            local start=0
            if [ "$total" -gt 10 ]; then
                start=$((total - 10))
            fi

            printf "  ${DIM}%-14s %-20s %-10s %s${NC}\n" "ID" "TIMESTAMP" "GIT SHA" "MESSAGE"

            local i
            for (( i=start; i<total; i++ )); do
                local entry="${lines[$i]}"
                local cp_id=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null)
                local cp_ts=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('timestamp','') or d.get('ts',''))" 2>/dev/null)
                local cp_sha=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print((d.get('git_sha','') or d.get('sha',''))[:8])" 2>/dev/null)
                local cp_msg=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print((d.get('message','') or d.get('task',''))[:50])" 2>/dev/null)

                if [ -n "$cp_id" ]; then
                    printf "  ${GREEN}%-14s${NC} %-20s ${CYAN}%-10s${NC} %s\n" "$cp_id" "$cp_ts" "$cp_sha" "$cp_msg"
                    count=$((count + 1))
                fi
            done

            if [ "$count" -eq 0 ]; then
                echo "  No valid checkpoints found."
            else
                echo ""
                echo "  Showing ${count} of ${total} checkpoints"
            fi
            ;;

        create)
            local raw_message="${*:-manual checkpoint}"
            # Escape backslashes and double quotes for JSON safety
            local message
            message=$(printf '%s' "$raw_message" | sed 's/\\/\\\\/g; s/"/\\"/g' | head -c 200)

            echo -e "${BOLD}Creating checkpoint...${NC}"
            echo ""

            # Ensure .loki exists
            if [ ! -d ".loki" ]; then
                echo -e "${RED}Error: No .loki directory found. Are you in a Loki project?${NC}"
                return 1
            fi

            # Capture git info
            local git_sha=""
            local git_branch=""
            if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
                git_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
                git_branch=$(git branch --show-current 2>/dev/null || echo "unknown")
            else
                git_sha="not-a-git-repo"
                git_branch="none"
            fi

            # Generate checkpoint ID with timestamp
            local ts=$(date -u '+%Y%m%d-%H%M%S')
            local cp_id="cp-${ts}"
            local cp_dir="$checkpoints_dir/$cp_id"

            # Create checkpoint directory
            mkdir -p "$cp_dir"

            # Copy state files
            local copied=0
            for item in .loki/session.json .loki/dashboard-state.json .loki/queue .loki/memory .loki/metrics .loki/council; do
                if [ -e "$item" ]; then
                    cp -r "$item" "$cp_dir/" 2>/dev/null && copied=$((copied + 1))
                fi
            done

            # Write metadata (use python3 json.dumps for safe serialization)
            local iso_ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
            # Truncate message to 500 chars to prevent abuse
            local safe_message="${message:0:500}"
            _CP_ID="$cp_id" _CP_TS="$iso_ts" _CP_SHA="$git_sha" _CP_BRANCH="$git_branch" \
            _CP_MSG="$safe_message" _CP_FILES="$copied" _CP_DIR="$cp_dir" _CP_INDEX="$index_file" \
            _CP_CHKDIR="$checkpoints_dir" python3 << 'WRITE_META_EOF'
import json, os
metadata = {
    "id": os.environ["_CP_ID"],
    "timestamp": os.environ["_CP_TS"],
    "git_sha": os.environ["_CP_SHA"],
    "git_branch": os.environ["_CP_BRANCH"],
    "message": os.environ["_CP_MSG"],
    "files_copied": int(os.environ["_CP_FILES"]),
    "created_by": "loki checkpoint create"
}
cp_dir = os.environ["_CP_DIR"]
os.makedirs(cp_dir, exist_ok=True)
with open(os.path.join(cp_dir, "metadata.json"), "w") as f:
    json.dump(metadata, f, indent=4)
index_file = os.environ["_CP_INDEX"]
os.makedirs(os.environ["_CP_CHKDIR"], exist_ok=True)
with open(index_file, "a") as f:
    index_entry = {
        "id": metadata["id"],
        "timestamp": metadata["timestamp"],
        "git_sha": metadata["git_sha"],
        "git_branch": metadata["git_branch"],
        "message": metadata["message"],
    }
    f.write(json.dumps(index_entry) + "\n")
WRITE_META_EOF

            echo -e "  Checkpoint: ${GREEN}$cp_id${NC}"
            echo -e "  Git SHA:    ${CYAN}${git_sha:0:8}${NC} ($git_branch)"
            echo -e "  Message:    $message"
            echo -e "  Files:      $copied state items copied"
            echo -e "  Location:   $cp_dir"
            echo ""
            echo "  Restore with: loki checkpoint rollback $cp_id"
            ;;

        show)
            local cp_id="${1:-}"
            if [ -z "$cp_id" ]; then
                echo -e "${RED}Error: Specify a checkpoint ID${NC}"
                echo "Usage: loki checkpoint show <id>"
                echo "Run 'loki checkpoint list' to see available checkpoints."
                return 1
            fi

            # Validate checkpoint ID (prevent path traversal)
            if [[ ! "$cp_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
                echo -e "${RED}Error: Invalid checkpoint ID (must be alphanumeric, hyphens, underscores only)${NC}"
                return 1
            fi

            local cp_dir="$checkpoints_dir/$cp_id"
            local metadata="$cp_dir/metadata.json"

            if [ ! -f "$metadata" ]; then
                echo -e "${RED}Error: Checkpoint not found: $cp_id${NC}"
                echo "Run 'loki checkpoint list' to see available checkpoints."
                return 1
            fi

            echo -e "${BOLD}Checkpoint: $cp_id${NC}"
            echo ""

            _CP_METADATA="$metadata" python3 << 'SHOW_EOF'
import json, os
with open(os.environ["_CP_METADATA"], "r") as f:
    d = json.load(f)
print(f"  ID:         {d.get('id', 'unknown')}")
print(f"  Timestamp:  {d.get('timestamp', 'unknown')}")
print(f"  Git SHA:    {d.get('git_sha', 'unknown')}")
print(f"  Git Branch: {d.get('git_branch', 'unknown')}")
print(f"  Message:    {d.get('message', 'none')}")
print(f"  Files:      {d.get('files_copied', 0)} state items")
print(f"  Created By: {d.get('created_by', 'unknown')}")
SHOW_EOF

            echo ""
            echo "  Contents:"
            for item in "$cp_dir"/*; do
                [ -e "$item" ] || continue
                local name=$(basename "$item")
                [ "$name" = "metadata.json" ] && continue
                if [ -d "$item" ]; then
                    local fcount=$(find "$item" -type f 2>/dev/null | wc -l | tr -d ' ')
                    echo -e "    ${DIM}[dir]${NC}  $name/ ($fcount files)"
                else
                    local fsize=$(wc -c < "$item" 2>/dev/null | tr -d ' ')
                    echo -e "    ${DIM}[file]${NC} $name (${fsize} bytes)"
                fi
            done
            ;;

        rollback)
            local cp_id="${1:-}"
            if [ -z "$cp_id" ]; then
                echo -e "${RED}Error: Specify a checkpoint ID${NC}"
                echo "Usage: loki checkpoint rollback <id>"
                echo "Run 'loki checkpoint list' to see available checkpoints."
                return 1
            fi

            # Validate checkpoint ID (prevent path traversal)
            if [[ ! "$cp_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
                echo -e "${RED}Error: Invalid checkpoint ID (must be alphanumeric, hyphens, underscores only)${NC}"
                return 1
            fi

            local cp_dir="$checkpoints_dir/$cp_id"
            local metadata="$cp_dir/metadata.json"

            if [ ! -f "$metadata" ]; then
                echo -e "${RED}Error: Checkpoint not found: $cp_id${NC}"
                echo "Run 'loki checkpoint list' to see available checkpoints."
                return 1
            fi

            echo -e "${BOLD}Rolling back to checkpoint: $cp_id${NC}"
            echo ""

            # Restore state files
            local restored=0
            for item in "$cp_dir"/*; do
                [ -e "$item" ] || continue
                local name
                name=$(basename "$item")
                [ "$name" = "metadata.json" ] && continue
                # R6: do not restore the worktree-snapshot sidecar as state.
                [ "$name" = "worktree-snapshot.txt" ] && continue

                if [ -d "$item" ]; then
                    # R6 data-loss fix: NEVER `rm -rf ".loki/$name"` -- the
                    # checkpoint store lives under .loki/state/checkpoints/, so
                    # blowing away .loki/state/ would destroy every checkpoint
                    # (including this one). Merge-copy directory contents instead.
                    mkdir -p ".loki/$name"
                    cp -r "$item"/. ".loki/$name"/ 2>/dev/null && restored=$((restored + 1))
                else
                    cp "$item" ".loki/$name" 2>/dev/null && restored=$((restored + 1))
                fi
            done

            echo -e "  Restored: ${GREEN}$restored${NC} state items from $cp_id"

            # Show how to also restore code. R6: prefer the anchored working-tree
            # snapshot over `git reset --hard <git_sha>`. git_sha is HEAD (the last
            # commit), and Loki does not commit per iteration, so a hard reset
            # discards the iteration's work instead of reconstructing it -- the old
            # hint was misleading.
            if git rev-parse --verify "refs/loki/cp/${cp_id}" >/dev/null 2>&1; then
                echo ""
                echo -e "  ${YELLOW}Note:${NC} Session state restored. Code is unchanged."
                echo "  To also restore the working tree to this checkpoint's snapshot:"
                echo ""
                echo -e "    ${DIM}git stash apply refs/loki/cp/${cp_id}${NC}"
                echo ""
                echo -e "  ${DIM}(restores tracked files; newly-added files are not removed)${NC}"
            else
                local cp_sha
                cp_sha=$(_CP_METADATA="$metadata" python3 -c "import json, os; d=json.load(open(os.environ['_CP_METADATA'])); print(d.get('git_sha','unknown'))" 2>/dev/null)
                if [ -n "$cp_sha" ] && [ "$cp_sha" != "unknown" ] && [ "$cp_sha" != "not-a-git-repo" ]; then
                    echo ""
                    echo -e "  ${YELLOW}Note:${NC} Session state restored, but no working-tree snapshot"
                    echo "  was captured for this checkpoint, so code changes since the last"
                    echo -e "  commit (${cp_sha:0:8}) are not restorable from here."
                fi
            fi
            ;;

        help|--help|-h)
            echo -e "${BOLD}loki checkpoint${NC} - Session state checkpoints"
            echo ""
            echo "Save and restore session state snapshots during autonomous runs."
            echo "Checkpoints capture .loki/ state files and record the git SHA"
            echo "at the time of creation."
            echo ""
            echo "Usage: loki checkpoint <command> [args]"
            echo "       loki cp <command> [args]"
            echo ""
            echo "Commands:"
            echo "  list              List recent checkpoints (default)"
            echo "  create [message]  Create a new checkpoint"
            echo "  show <id>         Show checkpoint details"
            echo "  rollback <id>     Restore state from a checkpoint"
            echo "  help              Show this help"
            echo ""
            echo "Examples:"
            echo "  loki checkpoint create 'before refactor'"
            echo "  loki cp list"
            echo "  loki cp show cp-20260212-143022"
            echo "  loki cp rollback cp-20260212-143022"
            ;;

        *)
            echo -e "${RED}Unknown checkpoint command: $subcommand${NC}"
            echo "Run 'loki checkpoint help' for usage."
            return 1
            ;;
    esac
}

# Poll `gh pr checks` while pending (rc=8); returns the final rc (0 = green).
_pr_checks_wait() {
    local pr="$1" poll="${LOKI_MERGE_POLL_S:-20}" tries="${LOKI_MERGE_MAX_POLLS:-90}" rc=8 i=0
    while [ "$rc" = "8" ] && [ "$i" -lt "$tries" ]; do
        rc=0; gh pr checks "$pr" >/dev/null 2>&1 || rc=$?
        [ "$rc" = "8" ] && { sleep "$poll"; i=$((i+1)); }
    done
    return "$rc"
}

# CI repair loop. Waits for checks; on red, hands the failed-run log tail to one
# bounded repair session on the PR branch (LOKI_PR_REPAIR_CMD, default: a
# `loki start` of a generated brief; it receives the evidence file path as its last argument), pushes,
# and re-checks, up to LOKI_PR_REPAIR_MAX attempts (default 2). Green goes through
# _merge_pr_gated (extra args pass through). Exhausted: the PR is returned to
# draft, nothing merges, and the last failure evidence is printed.
# Usage: _merge_pr_watch <pr> [--require-verified] [--delete-branch]
_merge_pr_watch() {
    local pr="$1" max="${LOKI_PR_REPAIR_MAX:-2}" attempt=0 rc branch cur rid evid tmp
    shift
    case "$max" in ''|*[!0-9]*) max=2 ;; esac
    command -v gh >/dev/null 2>&1 || { echo "$pr: not merged, gate: gh (gh CLI is required)" >&2; return 1; }
    branch="$(gh pr view "$pr" --json headRefName -q .headRefName 2>/dev/null)" || branch=""
    tmp="$(mktemp -d)" || return 1
    evid="$tmp/ci-failure.md"
    while true; do
        rc=0; _pr_checks_wait "$pr" || rc=$?
        if [ "$rc" = "0" ]; then
            rm -rf "$tmp"
            _merge_pr_gated "$pr" "$@"
            return $?
        fi
        rid="$(gh run list --branch "$branch" --status failure --limit 1 --json databaseId -q '.[0].databaseId' 2>/dev/null)" || rid=""
        {
            echo "# CI failed for PR $pr (branch ${branch:-unknown}); fix the failing checks, keep the change minimal"
            [ -n "$rid" ] && gh run view "$rid" --log-failed 2>/dev/null | tail -n 120
        } > "$evid"
        if [ "$attempt" -ge "$max" ]; then
            gh pr ready "$pr" --undo >/dev/null 2>&1 || true
            echo "$pr: not merged, gate: checks (gh pr checks rc=$rc after $attempt repair attempts); PR left as draft" >&2
            _pr_record_merge "$pr" "blocked:checks" "checks" "failing after $attempt repair attempts"
            echo "Last failure evidence:" >&2; sed 's/^/    /' "$evid" >&2
            rm -rf "$tmp"
            return 1
        fi
        attempt=$((attempt+1))
        cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
        if [ -z "$branch" ] || [ "$cur" != "$branch" ]; then
            echo "$pr: not merged, gate: repair (checkout is '${cur:-none}', repair must run on PR branch '${branch:-unknown}'); PR left open" >&2
            rm -rf "$tmp"; return 1
        fi
        echo "$pr: checks red, repair attempt $attempt of $max"
        if [ -n "${LOKI_PR_REPAIR_CMD:-}" ]; then
            bash -c "$LOKI_PR_REPAIR_CMD \"\$1\"" _ "$evid" || { echo "$pr: repair attempt $attempt failed, not pushing"; continue; }
        else
            "$(command -v loki || echo "$0")" start "$evid" || { echo "$pr: repair attempt $attempt failed, not pushing"; continue; }
        fi
        git push origin "HEAD:refs/heads/$branch" >/dev/null 2>&1 || echo "$pr: push of repair failed" >&2
    done
}

# Record the merge gate's own result in .loki/state/pr.json (merged,
# blocked:<gate> or waiting) so the receipt and handoff read the same outcome
# the gate produced. Only updates an existing "created" record that describes
# the same PR (pr arg matches .branch or the number at the end of .url); any
# other record is left untouched. Never invents one. File mode is preserved.
_pr_record_merge() {
    local pr="$1" outcome="$2" gate="${3:-}" checks="${4:-not recorded}" f=".loki/state/pr.json" t n
    n="${pr#\#}"
    case "$n" in */pull/*) n="${n%/}"; n="${n##*/}" ;; esac
    [ -f "$f" ] && command -v jq >/dev/null 2>&1 || return 0
    t="$(mktemp "$f.XXXXXX" 2>/dev/null)" || return 0
    cp -p "$f" "$t" 2>/dev/null || { rm -f "$t"; return 0; }
    if jq --arg p "$pr" --arg n "$n" --arg o "$outcome" --arg g "$gate" --arg c "$checks" \
        'select(.state=="created" and (.branch==$p or ((.url // "") | (endswith("/pull/" + $n) or endswith("/pull/" + $n + "/"))))) | .merge={outcome:$o, gate:(if $g=="" then null else $g end), checks:$c}' "$f" > "$t" 2>/dev/null && [ -s "$t" ]; then
        mv "$t" "$f"
    else
        rm -f "$t"
    fi
    return 0
}

# Owner opt-in earned merge (LOKI_EARNED_MERGE=1, off by default). Every path
# the PR changes (the new name and, for a rename, the previous name, listed by
# paginated REST and required to match the PR's changedFiles count) must have a
# Seal record from `loki trust --json` (path_seals) with at least
# LOKI_EARNED_MERGE_MIN_VERIFIED (default 20, floor 1) VERIFIED receipts and
# exactly 0 reverts in git history. A revert count of null (history
# unreadable), a missing record, an unreadable trust output or an unreadable or
# truncated file list or an unreadable diff all refuse, as does any PR whose
# diff touches a symlink (120000) or submodule (160000) mode, including a pure
# rename of one (modes read from git on the base and head). Protected paths (.github/, .loki/, .env*,
# .envrc, CODEOWNERS, plus the space-separated globs in
# LOKI_EARNED_MERGE_PROTECTED), matched case-insensitively, always stay
# PR-only. The reason for a refusal goes to stdout; the return code is 1.
_pr_earned_merge_check() {
    local pr="$1" min="${LOKI_EARNED_MERGE_MIN_VERIFIED:-20}" meta="" num="" want="" lines="" seals=""
    local kind p lp g row v r got=0 globs=() diff="" bad_mode="" renames="" refs="" base="" head="" from to m_old m_new
    case "$min" in ''|*[!0-9]*) min=20 ;; esac
    [ "$min" -ge 1 ] || min=1
    command -v jq >/dev/null 2>&1 || { echo "jq is required to read the Seal record"; return 1; }
    meta="$(gh pr view "$pr" --json number,changedFiles -q '"\(.number) \(.changedFiles)"' 2>/dev/null)" || meta=""
    num="${meta%% *}"; want="${meta#* }"
    case "$num$want" in ''|*[!0-9]*) echo "the PR's changed files are unreadable"; return 1 ;; esac
    lines="$(gh api --paginate "repos/{owner}/{repo}/pulls/${num}/files" -q '.[] | ("F\t" + .filename), (select(.previous_filename != null) | "P\t" + .previous_filename)' 2>/dev/null)" || lines=""
    [ -n "$lines" ] || { echo "the PR's changed files are unreadable"; return 1; }
    seals="$("$_LOKI_SCRIPT_DIR/../bin/loki" trust --json 2>/dev/null)" || seals=""
    printf '%s' "$seals" | jq -e '.path_seals | type == "array"' >/dev/null 2>&1 || { echo "the Seal record is unreadable"; return 1; }
    # read -a splits on whitespace without expanding globs against the cwd.
    read -r -a globs <<EOF_GLOBS
$(printf '%s' "${LOKI_EARNED_MERGE_PROTECTED:-}" | tr '[:upper:]' '[:lower:]')
EOF_GLOBS
    while IFS="$(printf '\t')" read -r kind p; do
        [ -n "$p" ] || continue
        [ "$kind" = "F" ] && got=$((got + 1))
        lp="$(printf '%s' "$p" | tr '[:upper:]' '[:lower:]')"
        case "$lp" in .github/*|.loki/*|.env|.env.*|*/.env|*/.env.*|.envrc|*/.envrc|codeowners|docs/codeowners)
            echo "$p is a protected path"; return 1 ;;
        esac
        for g in ${globs[@]+"${globs[@]}"}; do
            # shellcheck disable=SC2254
            case "$lp" in $g) echo "$p is a protected path"; return 1 ;; esac
        done
        row="$(printf '%s' "$seals" | jq -r --arg p "$p" '[.path_seals[] | select(.path == $p)] | if length == 0 then "none" else "\(.[0].verified) \(.[0].reverts)" end')" || row="none"
        [ "$row" != "none" ] || { echo "$p has no Seal record"; return 1; }
        v="${row%% *}"; r="${row#* }"
        case "$r" in
            0) ;;
            null) echo "$p revert status is unknown"; return 1 ;;
            *) echo "$p has $r recorded revert(s)"; return 1 ;;
        esac
        [ "$v" -ge "$min" ] 2>/dev/null || { echo "$p has $v VERIFIED receipts, need $min"; return 1; }
    done <<EOF_FILES
$lines
EOF_FILES
    [ "$got" = "$want" ] || { echo "the PR lists $want changed files but $got were readable"; return 1; }
    # A symlink (120000) or submodule (160000) can point an earned path anywhere:
    # refuse any PR whose diff adds, removes, changes or re-modes one.
    diff="$(gh pr diff "$pr" 2>/dev/null)" || diff=""
    [ -n "$diff" ] || { echo "the PR's file modes are unreadable"; return 1; }
    bad_mode="$(printf '%s\n' "$diff" | awk '/^diff --git /{h=$0} /^(new file mode|deleted file mode|old mode|new mode) (120000|160000)$/{print h; exit} /^index [0-9a-f.]+ (120000|160000)$/{print h; exit}')"
    [ -z "$bad_mode" ] || { echo "${bad_mode#diff --git } involves a symlink or submodule"; return 1; }
    # A pure rename (100% similarity) carries no mode or index line, so the scan
    # above cannot see a symlink moving. Read both sides' modes from git.
    renames="$(printf '%s\n' "$diff" | awk '/^rename from /{f=substr($0,13)} /^rename to /{print f "\t" substr($0,11); f=""}')"
    if [ -n "$renames" ]; then
        refs="$(gh pr view "$pr" --json baseRefOid,headRefOid -q '.baseRefOid + " " + .headRefOid' 2>/dev/null)" || refs=""
        base="${refs%% *}"; head="${refs#* }"
        case "$base$head" in ''|*[!0-9a-f]*) echo "the PR's rename modes are unreadable"; return 1 ;; esac
        if ! git cat-file -e "${base}^{commit}" 2>/dev/null || ! git cat-file -e "${head}^{commit}" 2>/dev/null; then
            GIT_TERMINAL_PROMPT=0 git fetch -q origin "pull/${num}/head" >/dev/null 2>&1 || true
        fi
        while IFS="$(printf '\t')" read -r from to; do
            [ -n "$to" ] || continue
            m_old="$(git ls-tree --full-tree "$base" -- "$from" 2>/dev/null | awk '{print $1; exit}')"
            m_new="$(git ls-tree --full-tree "$head" -- "$to" 2>/dev/null | awk '{print $1; exit}')"
            case "$m_old:$m_new" in
                120000:*|160000:*|*:120000|*:160000) echo "rename from $from to $to involves a symlink or submodule"; return 1 ;;
                '':*|*:'') echo "rename from $from to $to has unreadable file modes"; return 1 ;;
            esac
        done <<EOF_RENAMES
$renames
EOF_RENAMES
    fi
    return 0
}

# Shared merge gate. One PR (number or branch) merges only when it is not a
# draft, `gh pr checks` exits 0 after polling pending (rc=8), and, with
# --require-verified, every clean proof recorded for the PR's head commit
# (facts.git.head_sha == headRefOid, dirty-tree proofs ignored) says VERIFIED; a stale or missing proof
# refuses ("gate: head"). The merge passes --match-head-commit so a push after
# the gate cannot land unjudged. On refusal it prints the exact blocking gate
# ("gate: draft|head|verdict|trust|checks") and leaves the PR open.
# Usage: _merge_pr_gated <pr> [--require-verified] [--delete-branch]
_merge_pr_gated() {
    local pr="$1" need_verified=false margs=(--squash) rc d
    shift
    while [ $# -gt 0 ]; do
        case "$1" in
            --require-verified) need_verified=true ;;
            --delete-branch) margs+=(--delete-branch) ;;
        esac
        shift
    done
    command -v gh >/dev/null 2>&1 || { echo "$pr: not merged, gate: gh (gh CLI is required)" >&2; _pr_record_merge "$pr" "blocked:gh" "gh"; return 1; }
    d="$(gh pr view "$pr" --json isDraft -q .isDraft 2>/dev/null)" || d=""
    if [ "$d" != "false" ]; then
        echo "$pr: not merged, gate: draft (PR is a draft or its state is unreadable); PR left open" >&2
        _pr_record_merge "$pr" "blocked:draft" "draft"
        return 1
    fi
    # Pin the merge to the exact head the checks and proof are about to judge.
    local head=""
    head="$(gh pr view "$pr" --json headRefOid -q .headRefOid 2>/dev/null)" || head=""
    case "$head" in
        *[!0-9a-f]*) head="" ;;
    esac
    if [ "${#head}" != "40" ] && [ "${#head}" != "64" ]; then
        echo "$pr: not merged, gate: head (PR head commit is unreadable); PR left open" >&2
        _pr_record_merge "$pr" "blocked:head" "head"
        return 1
    fi
    if $need_verified; then
        local pid="" headline="" pj="" ph="" cands=() dirty_n=0 unknown_n=0
        # Resolve the proof recorded for THIS head: the run pointer first, then
        # any proof in .loki/proofs whose facts.git.head_sha equals the PR head.
        [ -f .loki/state/last-proof-id.txt ] && pid="$(cat .loki/state/last-proof-id.txt 2>/dev/null)"
        [ -z "$pid" ] || cands+=(".loki/proofs/${pid}/proof.json")
        cands+=(.loki/proofs/*/proof.json)
        if command -v jq >/dev/null 2>&1; then
            # Every clean proof for this head must agree: an older VERIFIED
            # never overrides a newer NOT_PROVEN. A proof taken on a dirty tree
            # (facts.git.dirty anything but literal false, incl. missing/null; .loki/ is excluded from that flag) judged
            # bytes other than the commit and is ignored.
            local hl="" disagree=false
            for pj in "${cands[@]}"; do
                [ -f "$pj" ] || continue
                ph="$(jq -r '.facts.git.head_sha // empty' "$pj" 2>/dev/null)"
                [ "$ph" = "$head" ] || continue
                case "$(jq -r 'if .facts.git.dirty == false then "clean" elif .facts.git.dirty == true then "dirty" else "unknown" end' "$pj" 2>/dev/null)" in
                    clean) ;;
                    dirty) dirty_n=$((dirty_n + 1)); continue ;;
                    *) unknown_n=$((unknown_n + 1)); continue ;;
                esac
                hl="$(jq -r '.honesty.headline // empty' "$pj" 2>/dev/null)"
                [ -n "$hl" ] || hl="UNREADABLE"
                if [ -z "$headline" ]; then headline="$hl"; elif [ "$headline" != "$hl" ]; then disagree=true; fi
            done
            if $disagree; then
                echo "$pr: not merged, gate: verdict (proofs for PR head ${head:0:12} disagree); PR left open" >&2
                _pr_record_merge "$pr" "blocked:verdict" "verdict"
                return 1
            fi
        fi
        if [ -z "$headline" ]; then
            echo "$pr: not merged, gate: head (no clean proof recorded for PR head ${head:0:12}; proof is stale, dirty ($dirty_n), dirty-unknown ($unknown_n) or missing); PR left open" >&2
            if [ "$unknown_n" -gt 0 ]; then
                echo "$pr: $unknown_n proof(s) for this head have an unknown dirty state (the proof predates the dirty flag, or git could not read the tree). Commit or stash local changes, then re-run 'loki start ...' on a clean tree for this head to record a clean proof." >&2
            fi
            _pr_record_merge "$pr" "blocked:head" "head"
            return 1
        fi
        if [ "$headline" != "VERIFIED" ]; then
            echo "$pr: not merged, gate: verdict (run verdict is '${headline}', need VERIFIED); PR left open" >&2
            _pr_record_merge "$pr" "blocked:verdict" "verdict"
            return 1
        fi
    fi
    if [ "${LOKI_EARNED_MERGE:-0}" = "1" ]; then
        local trust_why=""
        trust_why="$(_pr_earned_merge_check "$pr")" || {
            echo "$pr: not merged, gate: trust ($trust_why); PR left open" >&2
            _pr_record_merge "$pr" "blocked:trust" "trust"
            return 1
        }
    fi
    rc=0; _pr_checks_wait "$pr" || rc=$?
    if [ "$rc" != "0" ]; then
        echo "$pr: not merged, gate: checks (gh pr checks rc=$rc); PR left open" >&2
        case "$rc" in
            8) _pr_record_merge "$pr" "waiting" "checks" "pending" ;;
            1) _pr_record_merge "$pr" "blocked:checks" "checks" "failing" ;;
            *) _pr_record_merge "$pr" "blocked:checks" "checks" "unknown" ;;
        esac
        return 1
    fi
    if ! gh pr merge "$pr" "${margs[@]}" --match-head-commit "$head" >/dev/null 2>&1; then
        local head_now=""
        head_now="$(gh pr view "$pr" --json headRefOid -q .headRefOid 2>/dev/null)" || head_now=""
        if [ -n "$head_now" ] && [ "$head_now" != "$head" ]; then
            echo "$pr: not merged, gate: head (PR head moved from ${head:0:12} to ${head_now:0:12} during checks); PR left open" >&2
            _pr_record_merge "$pr" "blocked:head" "head" "passing"
            return 1
        fi
        echo "$pr: not merged, gate: merge (gh pr merge failed); PR left open" >&2
        _pr_record_merge "$pr" "blocked:merge" "merge" "passing"
        return 1
    fi
    _pr_record_merge "$pr" "merged" "" "all passing"
    return 0
}

# loki merge: serial PR merge queue. Queue lives in .loki/merge-queue (one PR
# number per line). A PR merges only when `gh pr checks` exits 0 (all green);
# after each merge the next PR is rebased onto the new base (gh pr update-branch
# --rebase) and must go green again before it merges. Never forces anything.
cmd_merge() {
    local sub="${1:-help}"
    shift 2>/dev/null || true
    local qfile=".loki/merge-queue"
    case "$sub" in
        add)
            [ $# -gt 0 ] || { echo "Usage: loki merge add <pr-number>..." >&2; return 2; }
            mkdir -p .loki || return 1
            local n
            for n in "$@"; do
                case "$n" in ''|*[!0-9]*) echo "Invalid PR number: $n" >&2; return 2 ;; esac
                if [ -f "$qfile" ] && grep -qx "$n" "$qfile"; then
                    echo "PR #$n already queued"
                else
                    echo "$n" >> "$qfile"
                    echo "Queued PR #$n"
                fi
            done
            ;;
        list|ls)
            if [ -s "$qfile" ]; then cat "$qfile" | sed 's/^/#/'; else echo "Merge queue is empty"; fi
            ;;
        clear)
            rm -f "$qfile"; echo "Merge queue cleared"
            ;;
        run)
            local dry=false
            [ "${1:-}" = "--dry-run" ] && dry=true
            if [ ! -s "$qfile" ]; then echo "Merge queue is empty"; return 0; fi
            command -v gh >/dev/null 2>&1 || { echo "gh CLI is required" >&2; return 1; }
            local first=true n rc remaining="" merged=0 blocked=0
            while IFS= read -r n; do
                [ -n "$n" ] || continue
                if $dry; then
                    rc=0; gh pr checks "$n" >/dev/null 2>&1 || rc=$?
                    case "$rc" in
                        0) echo "#$n: checks green, would merge (squash)" ;;
                        8) echo "#$n: checks pending, would wait" ;;
                        *) echo "#$n: checks not green, would skip" ;;
                    esac
                    $first || echo "#$n: would rebase onto new base first"
                    first=false
                    continue
                fi
                if ! $first; then
                    gh pr update-branch "$n" --rebase >/dev/null 2>&1 \
                        || { echo "#$n: rebase onto new base failed, left in queue"; remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue; }
                    echo "#$n: rebased onto new base"
                fi
                if _merge_pr_gated "$n" 2>"$qfile.err"; then
                    echo "#$n: merged"; merged=$((merged+1)); first=false
                else
                    sed 's/^/#/' "$qfile.err"; echo "#$n: left in queue"
                    remaining="$remaining$n"$'\n'; blocked=$((blocked+1))
                fi
            done < "$qfile"
            if $dry; then echo "Dry run: nothing changed"; return 0; fi
            rm -f "$qfile.err"
            printf '%s' "$remaining" > "$qfile"
            echo "Merged: $merged, left in queue: $blocked"
            [ "$blocked" -eq 0 ]
            ;;
        watch)
            [ $# -gt 0 ] || { echo "Usage: loki merge watch <pr> [--require-verified] [--delete-branch]" >&2; return 2; }
            _merge_pr_watch "$@" && echo "$1: merged"
            ;;
        pr)
            [ $# -gt 0 ] || { echo "Usage: loki merge pr <pr-or-branch> [--require-verified] [--delete-branch]" >&2; return 2; }
            _merge_pr_gated "$@" && echo "$1: merged"
            ;;
        help|--help|-h|*)
            echo "Usage: loki merge <add|list|run|pr|watch|clear>"
            echo "  watch <pr>         Wait for checks; on red run up to LOKI_PR_REPAIR_MAX (2) bounded"
            echo "                     repair sessions on the PR branch, push, re-check, merge when green."
            echo "  pr <pr|branch>     Merge one PR through the same gate (not draft, checks green;"
            echo "                     --require-verified also needs a VERIFIED run verdict)."
            echo "  add <pr>...        Queue pull requests"
            echo "  list               Show the queue"
            echo "  run [--dry-run]    Merge one at a time, only when checks are green;"
            echo "                     each next PR is rebased onto the new base first."
            echo "                     --dry-run lists what would happen, changes nothing."
            [ "$sub" = "help" ] || [ "$sub" = "--help" ] || [ "$sub" = "-h" ] || return 2
            ;;
    esac
}

# R6: one-command rollback. Top-level, obvious entry point that mirrors the Bun
# `loki rollback` subcommands (list/show/to/latest) so both routes are at parity.
# Restore is destructive on .loki/ state, so it ALWAYS captures a forced
# pre-rollback snapshot first (re-undoability invariant) and then glob-restores
# whatever the checkpoint dir contains (works across all three checkpoint writers).
# P12-48: back up and restore the machine-global dashboard state directory
# (audit log, retention checkpoint, config). Archive = tar.gz holding
# LOKI-BACKUP-MANIFEST (sha256 per file) plus the state tree, with a sidecar
# <archive>.sha256 over the whole archive. Restore verifies both before
# touching anything and refuses to overwrite existing state without --force.
_backup_state_dir() {
    printf '%s' "${LOKI_DASHBOARD_STATE_DIR:-${HOME}/.loki/dashboard}"
}

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

cmd_backup() {
    local dir out="" arg
    dir="$(_backup_state_dir)"
    while [ $# -gt 0 ]; do
        arg="$1"; shift
        case "$arg" in
            --dir) dir="${1:-}"; shift || true ;;
            --out) out="${1:-}"; shift || true ;;
            -h|--help)
                echo "Usage: loki backup [--dir STATE_DIR] [--out ARCHIVE.tar.gz]"
                echo "  Archives the dashboard state directory (default ~/.loki/dashboard) with a"
                echo "  sha256 manifest and writes ARCHIVE.sha256 next to it."
                return 0 ;;
            *) echo -e "${RED}Unknown option: $arg${NC}" >&2; return 2 ;;
        esac
    done
    if [ ! -d "$dir" ]; then
        echo -e "${RED}Error: state directory not found: $dir${NC}" >&2
        return 1
    fi
    [ -n "$out" ] || out="./loki-backup-$(date -u +%Y%m%d-%H%M%S).tar.gz"
    if [ -e "$out" ]; then
        echo -e "${RED}Error: refusing to overwrite existing archive: $out${NC}" >&2
        return 1
    fi
    dir="$(cd "$dir" && pwd -P)" || return 1

    local mdir manifest rel digest count=0
    mdir="$(mktemp -d "${TMPDIR:-/tmp}/loki-backup.XXXXXXXX")" || return 1
    manifest="$mdir/LOKI-BACKUP-MANIFEST"
    : > "$manifest"
    while IFS= read -r -d '' rel; do
        digest="$(_backup_sha256 "$dir/$rel")" || { rm -rf -- "$mdir"; return 1; }
        printf '%s  %s\n' "$digest" "$rel" >> "$manifest"
        count=$((count + 1))
    done < <(cd "$dir" && find . -type f ! -name '*.pid' -print0 | LC_ALL=C sort -z)

    # Symlinks and special files cannot be verified by the manifest; refuse them.
    if [ -n "$(cd "$dir" && find . ! -type f ! -type d ! -name '*.pid' -print -quit)" ]; then
        rm -rf -- "$mdir"
        echo -e "${RED}Error: $dir holds symlinks or special files; cannot back up safely${NC}" >&2
        return 1
    fi
    if ! tar -czf "$out" --exclude='*.pid' -C "$mdir" LOKI-BACKUP-MANIFEST -C "$dir" .; then
        rm -f -- "$out"
        rm -rf -- "$mdir"
        echo -e "${RED}Error: tar failed${NC}" >&2
        return 1
    fi
    rm -rf -- "$mdir"
    printf '%s  %s\n' "$(_backup_sha256 "$out")" "$(basename "$out")" > "${out}.sha256"
    echo "Backed up $count file(s) from $dir"
    echo "  archive: $out"
    echo "  sha256:  ${out}.sha256"
}

cmd_restore() {
    local archive="" dir force=0 expect="" arg
    dir="$(_backup_state_dir)"
    while [ $# -gt 0 ]; do
        arg="$1"; shift
        case "$arg" in
            --dir) dir="${1:-}"; shift || true ;;
            --force) force=1 ;;
            --sha256) expect="${1:-}"; shift || true ;;
            -h|--help)
                echo "Usage: loki restore ARCHIVE.tar.gz [--dir STATE_DIR] [--force] [--sha256 HEX]"
                echo "  Verifies the archive (sidecar or --sha256) and its manifest, then restores."
                echo "  Refuses to replace a non-empty STATE_DIR without --force; with --force the"
                echo "  existing directory is moved to STATE_DIR.pre-restore.<timestamp>."
                return 0 ;;
            -*) echo -e "${RED}Unknown option: $arg${NC}" >&2; return 2 ;;
            *) archive="$arg" ;;
        esac
    done
    if [ -z "$archive" ] || [ ! -f "$archive" ]; then
        echo -e "${RED}Error: archive not found. Usage: loki restore ARCHIVE.tar.gz${NC}" >&2
        return 2
    fi

    # 1. Whole-archive digest: explicit --sha256, else the sidecar. Required.
    if [ -z "$expect" ]; then
        if [ -f "${archive}.sha256" ]; then
            expect="$(awk 'NR==1 {print $1}' "${archive}.sha256")"
        else
            echo -e "${RED}Error: no ${archive}.sha256 sidecar and no --sha256; refusing unverified restore${NC}" >&2
            return 1
        fi
    fi
    if [ "$(_backup_sha256 "$archive")" != "$expect" ]; then
        echo -e "${RED}Error: archive sha256 mismatch (tampered or corrupt); nothing restored${NC}" >&2
        return 1
    fi

    # 2. Entry names must be plain relative paths with no symlinks or specials.
    local listing
    listing="$(tar -tzf "$archive" 2>/dev/null)" || { echo -e "${RED}Error: unreadable archive${NC}" >&2; return 1; }
    if printf '%s\n' "$listing" | grep -E -q '(^/|(^|/)\.\.(/|$))'; then
        echo -e "${RED}Error: archive contains unsafe paths; nothing restored${NC}" >&2
        return 1
    fi

    # 3. Extract to a staging dir beside the target (same filesystem), verify the manifest.
    mkdir -p "$(dirname "$dir")" || return 1
    local stage
    stage="$(mktemp -d "${dir}.restore.XXXXXXXX")" || return 1
    if ! tar -xzf "$archive" -C "$stage" 2>/dev/null; then
        rm -rf -- "$stage"
        echo -e "${RED}Error: extraction failed; nothing restored${NC}" >&2
        return 1
    fi
    local bad=""
    if [ ! -f "$stage/LOKI-BACKUP-MANIFEST" ]; then
        bad="missing manifest"
    elif [ -n "$(find "$stage" ! -type f ! -type d -print -quit)" ]; then
        bad="non-regular entry in archive"
    else
        local digest rel n_files n_listed
        while IFS= read -r line; do
            digest="${line%%  *}"; rel="${line#*  }"
            if [ ! -f "$stage/$rel" ] || [ "$(_backup_sha256 "$stage/$rel")" != "$digest" ]; then
                bad="manifest mismatch: $rel"; break
            fi
        done < "$stage/LOKI-BACKUP-MANIFEST"
        if [ -z "$bad" ]; then
            n_files="$(find "$stage" -type f ! -name LOKI-BACKUP-MANIFEST | wc -l | tr -d ' ')"
            n_listed="$(wc -l < "$stage/LOKI-BACKUP-MANIFEST" | tr -d ' ')"
            [ "$n_files" = "$n_listed" ] || bad="archive holds files the manifest does not list"
        fi
    fi
    if [ -n "$bad" ]; then
        rm -rf -- "$stage"
        echo -e "${RED}Error: archive failed verification ($bad); nothing restored${NC}" >&2
        return 1
    fi
    rm -f -- "$stage/LOKI-BACKUP-MANIFEST"

    # 4. Never overwrite existing state silently.
    if [ -e "$dir" ] && [ -n "$(ls -A "$dir" 2>/dev/null)" ]; then
        if [ "$force" -ne 1 ]; then
            rm -rf -- "$stage"
            echo -e "${RED}Error: $dir is not empty; re-run with --force to replace it${NC}" >&2
            return 1
        fi
        local aside
        aside="${dir}.pre-restore.$(date -u +%Y%m%d-%H%M%S)"
        mv "$dir" "$aside" || { rm -rf -- "$stage"; return 1; }
        echo "  previous state kept at: $aside"
    elif [ -d "$dir" ]; then
        rmdir "$dir" 2>/dev/null || true
    fi
    mv "$stage" "$dir" || return 1
    echo "Restored $dir from $archive (verified)"
}

cmd_rollback() {
    local subcommand="${1:-help}"
    shift 2>/dev/null || true

    local checkpoints_dir=".loki/state/checkpoints"
    local index_file="$checkpoints_dir/index.jsonl"

    # Resolve the most recent checkpoint id from on-disk cp-*/chk-* dirs, sorted
    # by mtime (newest last). Mirrors the Bun "latest = last entry" semantics but
    # is robust to the three differing id formats.
    _rollback_latest_id() {
        [ -d "$checkpoints_dir" ] || return 1
        ls -1dt "$checkpoints_dir"/*/ 2>/dev/null | head -1 | sed 's#/$##' | xargs -I{} basename {} 2>/dev/null
    }

    # Force a pre-rollback snapshot of current state, then glob-restore the target.
    # Delegates the actual restore to `cmd_checkpoint rollback`, which already
    # glob-restores and is shared with the manual path (no duplication).
    _rollback_restore() {
        local target_id="$1"
        local want_code="$2"

        # Validate id (defense in depth; cmd_checkpoint validates again).
        if [[ ! "$target_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
            echo -e "${RED}Error: Invalid checkpoint ID${NC}"
            return 1
        fi
        if [ ! -d "$checkpoints_dir/$target_id" ]; then
            echo -e "${RED}Error: Checkpoint not found: $target_id${NC}"
            echo "Run 'loki rollback list' to see available checkpoints."
            return 1
        fi

        # Re-undoability: snapshot current state before overwriting it.
        local pre_id
        pre_id="rb-pre-$(date -u '+%Y%m%d-%H%M%S')"
        local pre_dir="$checkpoints_dir/$pre_id"
        mkdir -p "$pre_dir"
        local saved=0
        for item in .loki/session.json .loki/dashboard-state.json .loki/CONTINUITY.md .loki/autonomy-state.json .loki/state .loki/queue; do
            if [ -e "$item" ]; then
                cp -r "$item" "$pre_dir/" 2>/dev/null && saved=$((saved + 1))
            fi
        done
        _RB_PID="$pre_id" _RB_TS="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" _RB_DIR="$pre_dir" \
        _RB_INDEX="$index_file" _RB_CHKDIR="$checkpoints_dir" python3 << 'RB_PRE_EOF' 2>/dev/null || true
import json, os
meta = {"id": os.environ["_RB_PID"], "timestamp": os.environ["_RB_TS"],
        "message": "pre-rollback snapshot", "created_by": "loki rollback"}
d = os.environ["_RB_DIR"]
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, "metadata.json"), "w") as f:
    json.dump(meta, f, indent=2)
os.makedirs(os.environ["_RB_CHKDIR"], exist_ok=True)
with open(os.environ["_RB_INDEX"], "a") as f:
    f.write(json.dumps({"id": meta["id"], "timestamp": meta["timestamp"], "message": meta["message"]}) + "\n")
RB_PRE_EOF
        echo -e "  ${DIM}Saved prior state as ${pre_id} (undo this rollback with: loki rollback to ${pre_id})${NC}"

        # Perform the actual state restore via the shared glob-restore path.
        cmd_checkpoint rollback "$target_id"

        # Optional code restore from the anchored working-tree snapshot.
        if [ "$want_code" = "1" ]; then
            if git rev-parse --verify "refs/loki/cp/${target_id}" >/dev/null 2>&1; then
                echo ""
                echo -e "  ${YELLOW}Restoring working tree from snapshot refs/loki/cp/${target_id}...${NC}"
                if git stash apply "refs/loki/cp/${target_id}" 2>/dev/null; then
                    echo -e "  ${GREEN}Working tree restored.${NC} (tracked files only; newly-added files were not removed)"
                else
                    echo -e "  ${RED}Could not apply snapshot cleanly.${NC} Resolve conflicts, or run: git stash apply refs/loki/cp/${target_id}"
                fi
            else
                echo ""
                echo -e "  ${YELLOW}No working-tree snapshot anchored for ${target_id}; code not restored.${NC}"
            fi
        fi
    }

    case "$subcommand" in
        list|ls)
            cmd_checkpoint list
            ;;
        show)
            cmd_checkpoint show "$@"
            ;;
        to)
            local target="${1:-}"
            local want_code=0
            shift 2>/dev/null || true
            for a in "$@"; do [ "$a" = "--code" ] && want_code=1; done
            if [ -z "$target" ]; then
                echo -e "${RED}Error: Specify a checkpoint ID${NC}"
                echo "Usage: loki rollback to <id> [--code]"
                echo "Run 'loki rollback list' to see available checkpoints."
                return 1
            fi
            echo -e "${BOLD}Rolling back to checkpoint: $target${NC}"
            echo ""
            _rollback_restore "$target" "$want_code"
            ;;
        latest)
            local want_code=0
            for a in "$@"; do [ "$a" = "--code" ] && want_code=1; done
            local latest_id
            latest_id=$(_rollback_latest_id)
            if [ -z "$latest_id" ]; then
                echo -e "${RED}No checkpoints found to roll back to.${NC}"
                return 1
            fi
            echo -e "${BOLD}Rolling back to latest checkpoint: $latest_id${NC}"
            echo ""
            _rollback_restore "$latest_id" "$want_code"
            ;;
        help|--help|-h)
            echo -e "${BOLD}loki rollback${NC} - One-command rollback to a checkpoint"
            echo ""
            echo "Restore .loki/ state and iteration/conversation context to a"
            echo "previous checkpoint. Every rollback first saves your current state"
            echo "as a pre-rollback snapshot, so you can always undo the undo."
            echo ""
            echo "Usage: loki rollback <command> [args]"
            echo ""
            echo "Commands:"
            echo "  list              List recent checkpoints"
            echo "  show <id>         Show checkpoint details"
            echo "  to <id> [--code]  Restore to checkpoint <id>"
            echo "  latest [--code]   Restore to the most recent checkpoint"
            echo ""
            echo "  --code            Also restore the working tree from the anchored"
            echo "                    git snapshot (tracked files only; overwrites them)."
            echo ""
            echo "Examples:"
            echo "  loki rollback list"
            echo "  loki rollback latest"
            echo "  loki rollback to cp-3-1717000000"
            echo "  loki rollback to cp-3-1717000000 --code"
            ;;
        *)
            echo -e "${RED}Unknown rollback command: $subcommand${NC}"
            echo "Run 'loki rollback help' for usage."
            return 1
            ;;
    esac
}

# Completion Council management
cmd_council() {
    local subcommand="${1:-status}"
    shift 2>/dev/null || true

    local council_dir=".loki/council"

    case "$subcommand" in
        status)
            echo -e "${CYAN}=== Completion Council Status ===${NC}"

            if [ ! -d "$council_dir" ]; then
                echo -e "${YELLOW}Council not initialized (no active session)${NC}"
                return 0
            fi

            if [ -f "$council_dir/state.json" ]; then
                LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
import json, os, sys
try:
    with open(os.environ['LOKI_COUNCIL_STATE']) as f:
        state = json.load(f)
    print(f\"Enabled: {state.get('initialized', False)}\")
    print(f\"Total votes: {state.get('total_votes', 0)}\")
    print(f\"Approve votes: {state.get('approve_votes', 0)}\")
    print(f\"Reject votes: {state.get('reject_votes', 0)}\")
    print(f\"Stagnation streak: {state.get('consecutive_no_change', 0)}\")
    print(f\"Done signals: {state.get('done_signals', 0)}\")
    print(f\"Last check: iteration {state.get('last_check_iteration', 'none')}\")
    verdicts = state.get('verdicts', [])
    if verdicts:
        print(f\"\nRecent verdicts:\")
        for v in verdicts[-5:]:
            print(f\"  Iteration {v['iteration']}: {v['result']} ({v['approve']} approve / {v['reject']} reject)\")
    else:
        print(f\"\nNo verdicts yet\")
except (json.JSONDecodeError, KeyError, TypeError) as e:
    print(f'Error: Council state file is corrupted or invalid: {e}', file=sys.stderr)
    print('Delete .loki/council/state.json and restart the session to reset.')
    sys.exit(1)
except Exception as e:
    print(f'Error reading council state: {e}', file=sys.stderr)
    sys.exit(1)
" 2>&1
            else
                echo "Council state file not found"
            fi
            ;;

        verdicts|log)
            echo -e "${CYAN}=== Council Decision Log ===${NC}"

            if [ -f "$council_dir/state.json" ]; then
                LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
import json, os, sys
try:
    with open(os.environ['LOKI_COUNCIL_STATE']) as f:
        state = json.load(f)
except (json.JSONDecodeError, KeyError, TypeError) as e:
    print(f'Error: Council state file is corrupted: {e}', file=sys.stderr)
    print('Delete .loki/council/state.json and restart the session to reset.')
    sys.exit(1)
verdicts = state.get('verdicts', [])
if not verdicts:
    print('No decisions recorded yet')
else:
    for v in verdicts:
        result = v.get('result', 'UNKNOWN')
        color = '\033[32m' if result == 'APPROVED' else '\033[31m'
        print(f\"{color}{result}\033[0m - Iteration {v['iteration']} ({v.get('timestamp', 'unknown')}) - {v['approve']} approve / {v['reject']} reject\")
" 2>/dev/null
            else
                echo "No council data available"
            fi
            ;;

        convergence)
            echo -e "${CYAN}=== Convergence Tracking ===${NC}"

            if [ -f "$council_dir/convergence.log" ]; then
                echo "Timestamp | Iteration | Files Changed | No-Change Streak | Done Signals"
                echo "----------|-----------|---------------|-----------------|-------------"
                tail -20 "$council_dir/convergence.log" | while IFS='|' read -r ts iter files streak signals; do
                    printf "%-10s| %-9s | %-13s | %-15s | %s\n" "$ts" "$iter" "$files" "$streak" "$signals"
                done
            else
                echo "No convergence data available"
            fi
            ;;

        force-review)
            echo "Requesting immediate council review..."
            mkdir -p ".loki/signals"
            echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > ".loki/signals/COUNCIL_REVIEW_REQUESTED"
            echo -e "${GREEN}Review requested. Council will convene at next iteration.${NC}"
            ;;

        report)
            if [ -f "$council_dir/report.md" ]; then
                cat "$council_dir/report.md"
            else
                echo "No completion report available yet"
            fi
            ;;

        config)
            echo -e "${CYAN}=== Council Configuration ===${NC}"
            echo "LOKI_COUNCIL_ENABLED=${LOKI_COUNCIL_ENABLED:-true}"
            echo "LOKI_COUNCIL_SIZE=${LOKI_COUNCIL_SIZE:-3}"
            echo "LOKI_COUNCIL_THRESHOLD=${LOKI_COUNCIL_THRESHOLD:-2}"
            echo "LOKI_COUNCIL_CHECK_INTERVAL=${LOKI_COUNCIL_CHECK_INTERVAL:-5}"
            echo "LOKI_COUNCIL_MIN_ITERATIONS=${LOKI_COUNCIL_MIN_ITERATIONS:-3}"
            echo "LOKI_COUNCIL_STAGNATION_LIMIT=${LOKI_COUNCIL_STAGNATION_LIMIT:-5}"
            ;;

        help|--help|-h)
            echo "Usage: loki council <command>"
            echo ""
            echo "Commands:"
            echo "  status         Show council status (default)"
            echo "  verdicts       Show decision log (alias: log)"
            echo "  convergence    Show convergence tracking data"
            echo "  force-review   Request immediate council review"
            echo "  report         Show completion report"
            echo "  config         Show council configuration"
            echo "  help           Show this help"
            ;;

        *)
            echo -e "${RED}Unknown council command: $subcommand${NC}"
            echo "Run 'loki council help' for usage."
            exit 1
            ;;
    esac
}

# Cross-project registry management
cmd_projects() {
    local subcommand="${1:-list}"
    local registry_dir="${HOME}/.loki/dashboard"
    local registry_file="${registry_dir}/projects.json"

    # Ensure directory exists
    mkdir -p "$registry_dir"

    # Initialize empty registry if needed
    if [ ! -f "$registry_file" ]; then
        echo '{"version":"1.0","projects":{}}' > "$registry_file"
    fi

    case "$subcommand" in
        list|ls)
            echo -e "${BOLD}Registered Projects${NC}"
            echo ""

            python3 -c "
import json
import os
from datetime import datetime

registry_file = '$registry_file'
if os.path.exists(registry_file):
    with open(registry_file, 'r') as f:
        data = json.load(f)
        projects = data.get('projects', {})
        if not projects:
            print('  (no projects registered)')
            print('')
            print('Use \"loki projects add <path>\" to register a project')
        else:
            # Sort by last accessed
            sorted_projects = sorted(
                projects.values(),
                key=lambda p: p.get('last_accessed') or p.get('registered_at', ''),
                reverse=True
            )
            for p in sorted_projects:
                status_icon = '[OK]' if os.path.isdir(p['path']) else '[MISSING]'
                alias = f\" ({p['alias']})\" if p.get('alias') else ''
                print(f\"  {status_icon} {p['name']}{alias}\")
                print(f\"      Path: {p['path']}\")
                if p.get('last_accessed'):
                    print(f\"      Last accessed: {p['last_accessed'][:10]}\")
                print()
else:
    print('  (no projects registered)')
" 2>/dev/null
            ;;

        add)
            local path="${2:-$(pwd)}"
            local name=""
            local alias=""
            shift 2 2>/dev/null || shift 1 2>/dev/null || true

            # Parse optional args
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --name|-n)
                        name="${2:-}"
                        if [ $# -ge 2 ]; then shift 2; else shift; fi
                        ;;
                    --alias|-a)
                        alias="${2:-}"
                        if [ $# -ge 2 ]; then shift 2; else shift; fi
                        ;;
                    *)
                        shift
                        ;;
                esac
            done

            # Resolve path
            path=$(cd "$path" 2>/dev/null && pwd || echo "$path")

            if [ ! -d "$path" ]; then
                echo -e "${RED}Error: Path does not exist: $path${NC}"
                exit 1
            fi

            # Route the read-modify-write through dashboard/registry.py so the
            # mutation is serialized under its fcntl lock and written atomically
            # (tempfile + os.replace), matching the v7.45.1 hardening that
            # run.sh registration and mark_project_stopped already use. The
            # inline open(...,'w') path here was unlocked and truncating, which
            # could lose a concurrent writer's entry or expose a 0-byte file to
            # a reader. Falls back to an atomic temp-file write if registry.py
            # cannot be imported at runtime (never a bare truncating open).
            _REGISTRY_FILE="$registry_file" _PROJ_PATH="$path" _PROJ_NAME="$name" _PROJ_ALIAS="$alias" \
            _REGISTRY_SKILL="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}" python3 -c "
import json
import os
import sys
import hashlib
import tempfile
from datetime import datetime, timezone

registry_file = os.environ['_REGISTRY_FILE']
path = os.environ['_PROJ_PATH']
name = os.environ['_PROJ_NAME'] or os.path.basename(path)
alias = os.environ['_PROJ_ALIAS'] or None

sys.path.insert(0, os.environ.get('_REGISTRY_SKILL', '.'))
try:
    from dashboard import registry
except Exception:
    registry = None

if registry is not None:
    existing = registry.get_project(path)
    project = registry.register_project(path, name, alias)
    print('Updated: ' + name if existing else 'Registered: ' + name)
else:
    # Fallback: atomic temp-file write (no truncating open) if registry.py
    # is unavailable. Mirrors register_project semantics (sha256 id).
    project_id = hashlib.sha256(path.encode()).hexdigest()[:12]
    try:
        with open(registry_file, 'r') as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError):
        data = {'version': '1.0', 'projects': {}}
    projects = data.get('projects', {})
    now = datetime.now(timezone.utc).isoformat()
    if project_id in projects:
        projects[project_id]['name'] = name
        if alias:
            projects[project_id]['alias'] = alias
        projects[project_id]['updated_at'] = now
        print(f'Updated: {name}')
    else:
        projects[project_id] = {
            'id': project_id,
            'path': path,
            'name': name,
            'alias': alias,
            'registered_at': now,
            'updated_at': now,
            'last_accessed': None,
            'has_loki_dir': os.path.isdir(os.path.join(path, '.loki')),
            'status': 'active',
        }
        print(f'Registered: {name}')
    data['projects'] = projects
    reg_dir = os.path.dirname(registry_file) or '.'
    os.makedirs(reg_dir, exist_ok=True)
    fd, tmp = tempfile.mkstemp(dir=reg_dir, prefix='.projects.', suffix='.tmp')
    try:
        with os.fdopen(fd, 'w') as f:
            json.dump(data, f, indent=2)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp, registry_file)
    except BaseException:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise

print(f'  Path: {path}')
if alias:
    print(f'  Alias: {alias}')
"
            ;;

        remove|rm)
            local identifier="${2:-}"

            if [ -z "$identifier" ]; then
                echo -e "${RED}Error: Specify project ID, path, or alias${NC}"
                exit 1
            fi

            # Route through registry.unregister_project so the delete is
            # serialized + atomic. Look the name up first to preserve the
            # "Removed: {name}" output. Falls back to an atomic temp-file write
            # if registry.py cannot be imported (never a truncating open).
            _REGISTRY_FILE="$registry_file" _IDENTIFIER="$identifier" \
            _REGISTRY_SKILL="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}" python3 -c "
import json
import os
import sys
import tempfile

registry_file = os.environ['_REGISTRY_FILE']
identifier = os.environ['_IDENTIFIER']

sys.path.insert(0, os.environ.get('_REGISTRY_SKILL', '.'))
try:
    from dashboard import registry
except Exception:
    registry = None

if registry is not None:
    existing = registry.get_project(identifier)
    if existing and registry.unregister_project(identifier):
        print('Removed: ' + existing['name'])
    else:
        print(f'Not found: {identifier}')
        sys.exit(1)
else:
    # Fallback: atomic temp-file write if registry.py is unavailable.
    try:
        with open(registry_file, 'r') as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError):
        data = {'version': '1.0', 'projects': {}}
    projects = data.get('projects', {})
    found_id = None
    for pid, project in projects.items():
        if pid == identifier or project['path'] == identifier or project.get('alias') == identifier:
            found_id = pid
            break
    if found_id:
        name = projects[found_id]['name']
        del projects[found_id]
        data['projects'] = projects
        reg_dir = os.path.dirname(registry_file) or '.'
        os.makedirs(reg_dir, exist_ok=True)
        fd, tmp = tempfile.mkstemp(dir=reg_dir, prefix='.projects.', suffix='.tmp')
        try:
            with os.fdopen(fd, 'w') as f:
                json.dump(data, f, indent=2)
                f.flush()
                os.fsync(f.fileno())
            os.replace(tmp, registry_file)
        except BaseException:
            try:
                os.unlink(tmp)
            except OSError:
                pass
            raise
        print(f'Removed: {name}')
    else:
        print(f'Not found: {identifier}')
        sys.exit(1)
"
            ;;

        discover)
            echo -e "${BOLD}Discovering Projects${NC}"
            echo ""

            python3 -c "
import os
from pathlib import Path

home = Path.home()
search_paths = [
    home / 'git',
    home / 'projects',
    home / 'code',
    home / 'dev',
    home / 'workspace',
    home / 'src',
]

discovered = []

def search_dir(path, depth=0, max_depth=3):
    if depth > max_depth:
        return
    try:
        if not path.is_dir():
            return
        loki_dir = path / '.loki'
        if loki_dir.is_dir():
            discovered.append({
                'path': str(path),
                'name': path.name,
                'has_state': (loki_dir / 'state' / 'session.json').exists(),
            })
            return
        for child in path.iterdir():
            if child.is_dir() and not child.name.startswith('.'):
                search_dir(child, depth + 1, max_depth)
    except (PermissionError, OSError):
        pass

for search_path in search_paths:
    if search_path.exists():
        search_dir(search_path)

if not discovered:
    print('  No projects with .loki directories found')
else:
    print(f'  Found {len(discovered)} project(s):')
    print('')
    for p in discovered:
        status = '[active]' if p['has_state'] else '[idle]'
        print(f\"  {status} {p['name']}\")
        print(f\"      {p['path']}\")
        print()
    print('Use \"loki projects sync\" to register all discovered projects')
" 2>/dev/null
            ;;

        sync)
            echo -e "${BOLD}Syncing Project Registry${NC}"
            echo ""

            # Route through registry.sync_registry_with_discovery so the
            # discover->merge->save is serialized + atomic and uses the same
            # sha256 ids as the rest of the registry. Reproduce the prior
            # stdout (per-project "Added:" lines + summary) from its return
            # value. Falls back to an atomic temp-file write if registry.py
            # cannot be imported (never a truncating open).
            _REGISTRY_FILE="$registry_file" \
            _REGISTRY_SKILL="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}" python3 -c "
import json
import os
import sys
import hashlib
import tempfile
from pathlib import Path
from datetime import datetime, timezone

registry_file = os.environ['_REGISTRY_FILE']

sys.path.insert(0, os.environ.get('_REGISTRY_SKILL', '.'))
try:
    from dashboard import registry
except Exception:
    registry = None

if registry is not None:
    result = registry.sync_registry_with_discovery()
    for project in result['details']['added']:
        print(f\"  Added: {project['name']}\")
    print('')
    print(f\"Added: {result['added']}, Missing: {result['missing']}, Total: {result['total']}\")
else:
    # Fallback: atomic temp-file write if registry.py is unavailable.
    home = Path.home()
    try:
        with open(registry_file, 'r') as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError):
        data = {'version': '1.0', 'projects': {}}
    projects = data.get('projects', {})
    search_paths = [
        home / 'git',
        home / 'projects',
        home / 'code',
        home / 'dev',
        home / 'workspace',
        home / 'src',
    ]
    discovered = []

    def search_dir(path, depth=0, max_depth=3):
        if depth > max_depth:
            return
        try:
            if not path.is_dir():
                return
            loki_dir = path / '.loki'
            if loki_dir.is_dir():
                discovered.append(str(path))
                return
            for child in path.iterdir():
                if child.is_dir() and not child.name.startswith('.'):
                    search_dir(child, depth + 1, max_depth)
        except (PermissionError, OSError):
            pass

    for search_path in search_paths:
        if search_path.exists():
            search_dir(search_path)

    added = 0
    now = datetime.now(timezone.utc).isoformat()
    for path in discovered:
        project_id = hashlib.sha256(path.encode()).hexdigest()[:12]
        if project_id not in projects:
            projects[project_id] = {
                'id': project_id,
                'path': path,
                'name': os.path.basename(path),
                'alias': None,
                'registered_at': now,
                'updated_at': now,
                'last_accessed': None,
                'has_loki_dir': True,
                'status': 'active',
            }
            added += 1
            print(f'  Added: {os.path.basename(path)}')

    missing = 0
    for pid, project in list(projects.items()):
        if not os.path.isdir(project['path']):
            project['status'] = 'missing'
            missing += 1

    data['projects'] = projects
    reg_dir = os.path.dirname(registry_file) or '.'
    os.makedirs(reg_dir, exist_ok=True)
    fd, tmp = tempfile.mkstemp(dir=reg_dir, prefix='.projects.', suffix='.tmp')
    try:
        with os.fdopen(fd, 'w') as f:
            json.dump(data, f, indent=2)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp, registry_file)
    except BaseException:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise

    print('')
    print(f'Added: {added}, Missing: {missing}, Total: {len(projects)}')
" 2>/dev/null
            ;;

        health)
            local identifier="${2:-$(pwd)}"

            _REGISTRY_FILE="$registry_file" _IDENTIFIER="$identifier" python3 -c "
import json
import os

registry_file = os.environ['_REGISTRY_FILE']
identifier = os.environ['_IDENTIFIER']

# If it's a path, resolve it
if os.path.isdir(identifier):
    identifier = os.path.abspath(identifier)

with open(registry_file, 'r') as f:
    data = json.load(f)

projects = data.get('projects', {})
project = None

for p in projects.values():
    if p['id'] == identifier or p['path'] == identifier or p.get('alias') == identifier:
        project = p
        break

if not project:
    print(f'Project not found: {identifier}')
    exit(1)

path = project['path']
checks = {
    'Path exists': os.path.isdir(path),
    '.loki dir exists': os.path.isdir(os.path.join(path, '.loki')),
    'Session state': os.path.isfile(os.path.join(path, '.loki', 'state', 'session.json')),
    'PRD exists': any(
        os.path.isfile(os.path.join(path, f))
        for f in ['PRD.md', 'prd.md', 'docs/PRD.md', 'docs/prd.md']
    ),
}

print(f\"Project: {project['name']}\")
print(f\"Path: {path}\")
print('')
print('Health Checks:')
for check, passed in checks.items():
    icon = '[OK]' if passed else '[FAIL]'
    print(f'  {icon} {check}')
"
            ;;

        --help|-h|help)
            echo -e "${BOLD}loki projects${NC} - Manage cross-project registry"
            echo ""
            echo "Usage: loki projects <command> [args]"
            echo ""
            echo "Commands:"
            echo "  list              List all registered projects"
            echo "  add <path>        Register a project"
            echo "  remove <id>       Remove a project from registry"
            echo "  discover          Find projects with .loki directories"
            echo "  sync              Auto-discover and register projects"
            echo "  health <id>       Check project health status"
            echo ""
            echo "Options for 'add':"
            echo "  --name, -n        Display name for the project"
            echo "  --alias, -a       Short alias for quick access"
            echo ""
            echo "Examples:"
            echo "  loki projects list"
            echo "  loki projects add . --alias myapp"
            echo "  loki projects add ~/git/project --name 'My Project'"
            echo "  loki projects discover"
            echo "  loki projects sync"
            echo "  loki projects health myapp"
            ;;

        *)
            echo -e "${RED}Unknown projects command: $subcommand${NC}"
            echo "Run 'loki projects help' for usage."
            exit 1
            ;;
    esac
}

# Voice input commands
cmd_voice() {
    # Voice mode is planned for a future release (#85)
    echo "Voice mode is planned for a future release. Track progress at github.com/asklokesh/loki-mode/issues/85"
    return 0
}

# Enterprise features (optional - requires env vars)
cmd_enterprise() {
    local subcommand="${1:-status}"
    local token_dir="${HOME}/.loki/dashboard"
    local token_file="${token_dir}/tokens.json"

    # Ensure directory exists
    mkdir -p "$token_dir"

    # Initialize empty tokens if needed
    if [ ! -f "$token_file" ]; then
        echo '{"version":"1.0","tokens":{}}' > "$token_file"
        chmod 600 "$token_file"
    fi

    case "$subcommand" in
        status)
            echo -e "${BOLD}Enterprise Features Status${NC}"
            echo ""

            local auth_enabled="${LOKI_ENTERPRISE_AUTH:-false}"
            local audit_disabled="${LOKI_AUDIT_DISABLED:-false}"
            local audit_force_on="${LOKI_ENTERPRISE_AUDIT:-false}"

            if [ "$auth_enabled" = "true" ] || [ "$auth_enabled" = "1" ]; then
                echo -e "  Token Auth:     ${GREEN}Enabled${NC}"
            else
                echo -e "  Token Auth:     ${DIM}Disabled${NC}"
            fi

            # OIDC/SSO status
            local oidc_issuer="${LOKI_OIDC_ISSUER:-}"
            local oidc_client="${LOKI_OIDC_CLIENT_ID:-}"
            if [ -n "$oidc_issuer" ] && [ -n "$oidc_client" ]; then
                echo -e "  OIDC/SSO:       ${GREEN}Enabled${NC} (${oidc_issuer})"
            else
                echo -e "  OIDC/SSO:       ${DIM}Disabled${NC}"
            fi

            # Audit is on by default; disabled only if LOKI_AUDIT_DISABLED=true
            if [ "$audit_force_on" = "true" ] || [ "$audit_force_on" = "1" ]; then
                echo -e "  Audit Logging:  ${GREEN}Enabled (enterprise)${NC}"
            elif [ "$audit_disabled" = "true" ] || [ "$audit_disabled" = "1" ]; then
                echo -e "  Audit Logging:  ${DIM}Disabled${NC}"
            else
                echo -e "  Audit Logging:  ${GREEN}Enabled (default)${NC}"
            fi

            echo ""
            echo "Configure with environment variables:"
            echo "  export LOKI_ENTERPRISE_AUTH=true     # Token authentication"
            echo "  export LOKI_AUDIT_DISABLED=true      # Disable audit logging"
            echo "  export LOKI_ENTERPRISE_AUDIT=true    # Force audit on (legacy)"
            echo ""
            echo "OIDC/SSO (optional, works alongside token auth):"
            echo "  export LOKI_OIDC_ISSUER=https://accounts.google.com"
            echo "  export LOKI_OIDC_CLIENT_ID=your-client-id"
            echo "  export LOKI_OIDC_AUDIENCE=your-audience    # Optional, defaults to client_id"
            ;;

        token)
            local token_cmd="${2:-list}"
            shift 2 2>/dev/null || shift 1 2>/dev/null || true

            case "$token_cmd" in
                generate|create|new)
                    local name="${1:-}"
                    local scopes=""
                    local expires=""

                    if [ -z "$name" ]; then
                        echo -e "${RED}Error: Token name required${NC}"
                        echo "Usage: loki enterprise token generate <name> [--scopes '*'] [--expires 30]"
                        exit 2
                    fi

                    # Validate name is not a flag
                    if [[ "$name" == -* ]]; then
                        echo -e "${RED}Error: Token name cannot start with '-'${NC}"
                        echo "Usage: loki enterprise token generate <name> [--scopes '*'] [--expires 30]"
                        exit 1
                    fi

                    # Warn if enterprise auth is not enabled
                    local auth_enabled="${LOKI_ENTERPRISE_AUTH:-false}"
                    if [ "$auth_enabled" != "true" ] && [ "$auth_enabled" != "1" ]; then
                        echo -e "${YELLOW}Warning: LOKI_ENTERPRISE_AUTH is not enabled${NC}"
                        echo "Tokens can be generated but won't work for API authentication."
                        echo "Enable with: export LOKI_ENTERPRISE_AUTH=true"
                        echo ""
                    fi

                    shift

                    while [[ $# -gt 0 ]]; do
                        case "$1" in
                            --scopes|-s)
                                scopes="${2:-}"
                                shift 2
                                ;;
                            --expires|-e)
                                expires="${2:-}"
                                shift 2
                                ;;
                            *)
                                shift
                                ;;
                        esac
                    done

                    _TOKEN_FILE="$token_file" _TOKEN_NAME="$name" _TOKEN_SCOPES="$scopes" _TOKEN_EXPIRES="$expires" python3 -c "
import json
import secrets
import hashlib
from datetime import datetime, timezone, timedelta
import os

token_file = os.environ['_TOKEN_FILE']
name = os.environ['_TOKEN_NAME']
scopes_str = os.environ['_TOKEN_SCOPES']
expires_str = os.environ['_TOKEN_EXPIRES']

# Parse scopes
scopes = scopes_str.split(',') if scopes_str else ['*']

# Parse expiration
expires_at = None
if expires_str:
    days = int(expires_str)
    expires_at = (datetime.now(timezone.utc) + timedelta(days=days)).isoformat()

# Generate token
raw_token = f'loki_{secrets.token_urlsafe(32)}'
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
token_id = token_hash[:12]

# Load tokens
with open(token_file, 'r') as f:
    data = json.load(f)

tokens = data.get('tokens', {})

# Check duplicate name
for existing in tokens.values():
    if existing.get('name') == name:
        print(f'Error: Token with name \"{name}\" already exists')
        exit(1)

# Create token entry
tokens[token_id] = {
    'id': token_id,
    'name': name,
    'hash': token_hash,
    'scopes': scopes,
    'created_at': datetime.now(timezone.utc).isoformat(),
    'expires_at': expires_at,
    'last_used': None,
    'revoked': False,
}

data['tokens'] = tokens
with open(token_file, 'w') as f:
    json.dump(data, f, indent=2)

# Set restrictive permissions
os.chmod(token_file, 0o600)

print(f'Token created: {name}')
print(f'ID: {token_id}')
print(f'Scopes: {scopes}')
if expires_at:
    print(f'Expires: {expires_at[:10]}')
print('')
print('Token (save this - shown only once):')
print(f'  {raw_token}')
"
                    ;;

                list|ls)
                    local include_revoked="false"
                    if [ "${1:-}" = "--all" ] || [ "${1:-}" = "--include-revoked" ]; then
                        include_revoked="true"
                    fi

                    echo -e "${BOLD}API Tokens${NC}"
                    if [ "$include_revoked" = "true" ]; then
                        echo -e "${DIM}(including revoked)${NC}"
                    fi
                    echo ""

                    python3 -c "
import json
import os

token_file = '$token_file'
include_revoked = '$include_revoked' == 'true'

with open(token_file, 'r') as f:
    data = json.load(f)

tokens = data.get('tokens', {})
if include_revoked:
    token_list = list(tokens.values())
else:
    token_list = [t for t in tokens.values() if not t.get('revoked')]

if not token_list:
    print('  (no tokens)')
    if not include_revoked:
        print('')
        print('  Use --all to include revoked tokens')
else:
    for t in token_list:
        if t.get('revoked'):
            status = '[REVOKED]'
        elif t.get('expires_at'):
            from datetime import datetime, timezone
            expires = datetime.fromisoformat(t['expires_at'])
            if datetime.now(timezone.utc) > expires:
                status = '[EXPIRED]'
            else:
                status = '[OK]'
        else:
            status = '[OK]'
        print(f\"  {status} {t['name']} ({t['id']})\")
        print(f\"      Scopes: {', '.join(t['scopes'])}\")
        if t.get('created_at'):
            print(f\"      Created: {t['created_at'][:10]}\")
        if t.get('expires_at'):
            print(f\"      Expires: {t['expires_at'][:10]}\")
        if t.get('last_used'):
            print(f\"      Last used: {t['last_used'][:10]}\")
        print()
" 2>/dev/null
                    ;;

                revoke)
                    local identifier="${1:-}"

                    if [ -z "$identifier" ]; then
                        echo -e "${RED}Error: Token ID or name required${NC}"
                        exit 2
                    fi

                    _TOKEN_FILE="$token_file" _IDENTIFIER="$identifier" python3 -c "
import json, os
from datetime import datetime, timezone

token_file = os.environ['_TOKEN_FILE']
identifier = os.environ['_IDENTIFIER']

with open(token_file, 'r') as f:
    data = json.load(f)

tokens = data.get('tokens', {})
found_id = None

for tid, token in tokens.items():
    if tid == identifier or token.get('name') == identifier:
        found_id = tid
        break

if found_id:
    tokens[found_id]['revoked'] = True
    tokens[found_id]['revoked_at'] = datetime.now(timezone.utc).isoformat()
    data['tokens'] = tokens
    with open(token_file, 'w') as f:
        json.dump(data, f, indent=2)
    print(f'Revoked: {tokens[found_id][\"name\"]}')
else:
    print(f'Token not found: {identifier}')
    exit(1)
"
                    ;;

                delete)
                    local identifier="${1:-}"

                    if [ -z "$identifier" ]; then
                        echo -e "${RED}Error: Token ID or name required${NC}"
                        exit 2
                    fi

                    _TOKEN_FILE="$token_file" _IDENTIFIER="$identifier" python3 -c "
import json, os

token_file = os.environ['_TOKEN_FILE']
identifier = os.environ['_IDENTIFIER']

with open(token_file, 'r') as f:
    data = json.load(f)

tokens = data.get('tokens', {})
found_id = None
found_name = None

for tid, token in tokens.items():
    if tid == identifier or token.get('name') == identifier:
        found_id = tid
        found_name = token.get('name')
        break

if found_id:
    del tokens[found_id]
    data['tokens'] = tokens
    with open(token_file, 'w') as f:
        json.dump(data, f, indent=2)
    print(f'Deleted: {found_name}')
else:
    print(f'Token not found: {identifier}')
    exit(1)
"
                    ;;

                *)
                    echo -e "${RED}Unknown token command: $token_cmd${NC}"
                    echo ""
                    echo "Usage: loki enterprise token <command>"
                    echo ""
                    echo "Commands:"
                    echo "  generate <name>   Create a new API token"
                    echo "  list              List all active tokens"
                    echo "  revoke <id>       Revoke a token (keeps record)"
                    echo "  delete <id>       Permanently delete a token"
                    echo ""
                    echo "Options for 'generate':"
                    echo "  --scopes, -s      Comma-separated scopes (default: *)"
                    echo "  --expires, -e     Days until expiration (default: never)"
                    exit 1
                    ;;
            esac
            ;;

        audit)
            local audit_cmd="${2:-summary}"

            # Audit is on by default. Only blocked if explicitly disabled and not force-enabled.
            local _audit_disabled="${LOKI_AUDIT_DISABLED:-false}"
            local _audit_force="${LOKI_ENTERPRISE_AUDIT:-false}"
            if [ "$_audit_force" != "true" ] && [ "$_audit_force" != "1" ]; then
                if [ "$_audit_disabled" = "true" ] || [ "$_audit_disabled" = "1" ]; then
                    echo -e "${YELLOW}Audit logging is disabled${NC}"
                    echo "Remove LOKI_AUDIT_DISABLED or set LOKI_ENTERPRISE_AUDIT=true"
                    exit 1
                fi
            fi

            local audit_dir="${HOME}/.loki/dashboard/audit"

            case "$audit_cmd" in
                summary)
                    echo -e "${BOLD}Audit Summary (Last 7 Days)${NC}"
                    echo ""

                    if [ ! -d "$audit_dir" ]; then
                        echo "  No audit logs found"
                        exit 0
                    fi

                    python3 -c "
import json
import os
from pathlib import Path
from datetime import datetime, timezone, timedelta

audit_dir = Path('$audit_dir')
days = 7
start_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime('%Y-%m-%d')

entries = []
for log_file in sorted(audit_dir.glob('audit-*.jsonl')):
    if log_file.stem >= f'audit-{start_date}':
        try:
            with open(log_file, 'r') as f:
                for line in f:
                    try:
                        entries.append(json.loads(line.strip()))
                    except json.JSONDecodeError:
                        pass
        except IOError:
            pass

total = len(entries)
success = sum(1 for e in entries if e.get('success', True))
failed = total - success

print(f'Total events: {total}')
print(f'Successful: {success}')
print(f'Failed: {failed}')
print('')

if failed > 0:
    print('Recent failures:')
    failures = [e for e in entries if not e.get('success', True)][-5:]
    for f in failures:
        print(f\"  {f.get('timestamp', '')[:19]} {f.get('action')} {f.get('resource_type')}: {f.get('error', 'unknown')}\")
" 2>/dev/null
                    ;;

                tail)
                    echo -e "${BOLD}Recent Audit Events${NC}"
                    echo ""

                    if [ ! -d "$audit_dir" ]; then
                        echo "  No audit logs found"
                        exit 0
                    fi

                    # Get most recent log file and show last 20 entries
                    local latest_log
                    latest_log=$(ls -t "$audit_dir"/audit-*.jsonl 2>/dev/null | head -1)

                    if [ -z "$latest_log" ]; then
                        echo "  No audit logs found"
                        exit 0
                    fi

                    tail -20 "$latest_log" | python3 -c "
import json
import sys

for line in sys.stdin:
    try:
        e = json.loads(line.strip())
        status = 'OK' if e.get('success', True) else 'FAIL'
        print(f\"[{status}] {e.get('timestamp', '')[:19]} {e.get('action')} {e.get('resource_type')} {e.get('resource_id', '')}\")
    except:
        pass
" 2>/dev/null
                    ;;

                *)
                    echo -e "${RED}Unknown audit command: $audit_cmd${NC}"
                    echo ""
                    echo "Usage: loki enterprise audit <command>"
                    echo ""
                    echo "Commands:"
                    echo "  summary           Show audit summary (last 7 days)"
                    echo "  tail              Show recent audit events"
                    exit 1
                    ;;
            esac
            ;;

        --help|-h|help)
            echo -e "${BOLD}loki enterprise${NC} - Enterprise features (optional)"
            echo ""
            echo "Usage: loki enterprise <command>"
            echo ""
            echo "Commands:"
            echo "  status            Show which enterprise features are enabled"
            echo "  token <cmd>       Manage API tokens (requires LOKI_ENTERPRISE_AUTH=true)"
            echo "  audit <cmd>       Query audit logs (enabled by default)"
            echo ""
            echo "Configure enterprise features:"
            echo "  export LOKI_ENTERPRISE_AUTH=true     # Token authentication"
            echo "  export LOKI_AUDIT_DISABLED=true      # Disable audit logging"
            echo "  export LOKI_ENTERPRISE_AUDIT=true    # Force audit on (legacy)"
            echo ""
            echo "OIDC/SSO (optional, works alongside token auth):"
            echo "  export LOKI_OIDC_ISSUER=https://accounts.google.com"
            echo "  export LOKI_OIDC_CLIENT_ID=your-client-id"
            echo "  export LOKI_OIDC_AUDIENCE=your-audience    # Optional"
            echo ""
            echo "Audit logging is enabled by default. Auth is optional."
            ;;

        *)
            echo -e "${RED}Unknown enterprise command: $subcommand${NC}"
            echo "Run 'loki enterprise help' for usage."
            exit 1
            ;;
    esac
}

# Telemetry management (v6.7.0)
cmd_crash() {
    # Phase 0: local-only crash report inspection. NO network egress.
    #
    # PARITY: the user-facing strings here MUST match the TS canonical command
    # at loki-ts/src/commands/crash.ts byte-for-byte (ignoring color codes).
    # The whole subcommand body is implemented in one Python helper so the
    # behavior (filename-id resolution, "-" for missing fields, JSON shape,
    # URL encoding, exit codes) cannot drift from the TS route.
    #
    # Subcommands:
    #   loki crash               list .loki/crash/*.json reports
    #   loki crash show <id>     pretty-print one scrubbed report
    #   loki crash submit [<id>] print the scrubbed payload + a prefilled
    #                            GitHub issue URL for MANUAL submission
    _LOKI_CRASH_DIR=".loki/crash" _LOKI_CRASH_SUB="${1-}" _LOKI_CRASH_ARG="${2-}" \
    python3 -c '
import json, os, sys, urllib.parse

# Colors: honor NO_COLOR (https://no-color.org) exactly like the TS route
# (loki-ts/src/util/colors.ts). Codes match autonomy/loki:25-32 byte-for-byte.
_no_color = len(os.environ.get("NO_COLOR", "")) > 0
def _c(code):
    return "" if _no_color else code
RED = _c("\x1b[0;31m")
GREEN = _c("\x1b[0;32m")
YELLOW = _c("\x1b[1;33m")
CYAN = _c("\x1b[0;36m")
BOLD = _c("\x1b[1m")
NC = _c("\x1b[0m")

ISSUE_BASE = "https://github.com/asklokesh/loki-mode/issues/new"
CRASH_DIR = os.environ["_LOKI_CRASH_DIR"]
SUB = os.environ.get("_LOKI_CRASH_SUB", "")
ARG = os.environ.get("_LOKI_CRASH_ARG", "")

HELP = (
    BOLD + "loki crash" + NC + " - inspect and manually submit local crash reports\n"
    "\n"
    "Usage: loki crash [subcommand] [args]\n"
    "\n"
    "Subcommands:\n"
    "  (none) | list        List crash reports in .loki/crash/\n"
    "  show <id>            Pretty-print one scrubbed crash report\n"
    "  submit [<id>]        Print the scrubbed payload and a prefilled GitHub\n"
    "                       issue URL for manual submission\n"
    "\n"
    "Crash reports are anonymous, scrubbed, and stored locally only. Nothing is\n"
    "sent automatically in this version. See docs/PRIVACY.md.\n"
)

def s(v):
    return "-" if v is None else str(v)

def pad(text, w):
    return text if len(text) >= w else text + " " * (w - len(text))

def report_ids():
    if not os.path.isdir(CRASH_DIR):
        return []
    try:
        names = [e for e in os.listdir(CRASH_DIR)
                 if e.endswith(".json") and os.path.isfile(os.path.join(CRASH_DIR, e))]
    except OSError:
        return []
    return sorted(n[:-len(".json")] for n in names)

def _safe_id(rid):
    # Reject path traversal: ids are bare filenames (no separators, no "..",
    # no leading separator). This guards the direct filename lookup; the
    # fingerprint-resolution loop only ever passes real listdir ids, which
    # always pass. Mirrors the identical rule in loki-ts crash.ts.
    if rid is None or rid == "":
        return False
    if "/" in rid or "\\" in rid or ".." in rid:
        return False
    if rid[0] in ("/", "\\"):
        return False
    return True

def read_report(rid):
    if not _safe_id(rid):
        return None
    p = os.path.join(CRASH_DIR, rid + ".json")
    if not os.path.exists(p):
        return None
    try:
        with open(p) as f:
            return json.load(f)
    except Exception:
        return {}

def resolve_report(arg):
    direct = read_report(arg)
    if direct is not None:
        return (arg, direct)
    for rid in report_ids():
        r = read_report(rid)
        if r is not None and str(r.get("fingerprint", "")) == arg:
            return (rid, r)
    return None

def dumps(obj):
    return json.dumps(obj, indent=2, ensure_ascii=False)

def list_crashes():
    ids = report_ids()
    if not ids:
        sys.stdout.write(YELLOW + "No crash reports found." + NC +
                         " Nothing has been captured in .loki/crash/.\n")
        return 0
    sys.stdout.write(pad("ID", 40) + "  " + pad("CAPTURED_AT", 22) + "  ERROR_CLASS\n")
    for rid in ids:
        d = read_report(rid) or {}
        fp = s(d.get("fingerprint"))
        captured_at = s(d.get("captured_at"))
        error_class = s(d.get("error_class"))
        visible_id = fp if fp != "-" else rid
        sys.stdout.write(pad(visible_id, 40) + "  " + pad(captured_at, 22) + "  " + error_class + "\n")
    sys.stdout.write(
        "\n" + str(len(ids)) + " report(s). Run \x27loki crash show <id>\x27 to inspect, "
        "\x27loki crash submit\x27 to get a prefilled GitHub issue URL.\n")
    return 0

def show_crash(rid):
    if not rid:
        sys.stderr.write(RED + "Missing crash id." + NC + " Use \x27loki crash\x27 to list reports.\n")
        return 2
    resolved = resolve_report(rid)
    if resolved is None:
        sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
        sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
        return 1
    sys.stdout.write(dumps(resolved[1]) + "\n")
    return 0

def issue_url(report):
    error_class = s(report.get("error_class"))
    fp = s(report.get("fingerprint"))
    short_fp = fp[:12] if fp != "-" else "unknown"
    title = "crash: " + error_class + " (" + short_fp + ")"
    payload = dumps(report)
    body = "\n".join([
        "Anonymous crash report captured by Loki Mode (scrubbed, whitelist-only).",
        "",
        "Scrubbed payload:",
        "```json",
        payload,
        "```",
        "",
        "Nothing was sent automatically. This issue is submitted manually by me.",
    ])
    q = urllib.parse.urlencode({"title": title, "body": body})
    return ISSUE_BASE + "?" + q

def submit_crash(rid):
    if rid:
        target = resolve_report(rid)
        if target is None:
            sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
            sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
            return 1
    else:
        ids = report_ids()
        if not ids:
            sys.stdout.write(YELLOW + "No crash reports found." + NC + " Nothing to submit.\n")
            return 0
        last_id = ids[-1]
        target = (last_id, read_report(last_id) or {})
    sys.stdout.write(BOLD + "Scrubbed payload (this is the ENTIRE report):" + NC + "\n")
    sys.stdout.write(dumps(target[1]) + "\n\n")
    sys.stdout.write(YELLOW + "Nothing is sent automatically in this version." + NC +
                     " Loki Mode never transmits crash data on its own.\n")
    sys.stdout.write("To submit manually, open this prefilled GitHub issue and review it first:\n\n")
    sys.stdout.write("  " + CYAN + issue_url(target[1]) + NC + "\n\n")
    sys.stdout.write(GREEN + "The payload above is exactly what the URL contains." + NC + "\n")
    sys.stdout.write("See docs/PRIVACY.md for what is and is not collected.\n")
    return 0

if SUB == "" or SUB == "list":
    sys.exit(list_crashes())
elif SUB in ("--help", "-h", "help"):
    sys.stdout.write(HELP)
    sys.exit(0)
elif SUB == "show":
    sys.exit(show_crash(ARG))
elif SUB == "submit":
    sys.exit(submit_crash(ARG))
else:
    sys.stderr.write(RED + "Unknown crash subcommand: " + SUB + NC + "\n")
    sys.stdout.write(HELP)
    sys.exit(2)
'
    return $?
}

cmd_telemetry() {
    local subcommand="${1:-status}"
    shift 2>/dev/null || true

    case "$subcommand" in
        status)
            echo -e "${BOLD}Telemetry Status${NC}"
            echo ""

            # Check persistent opt-out (~/.loki/config)
            local global_config="${HOME}/.loki/config"
            local persistently_disabled=false
            if [ -f "$global_config" ] && grep -q "^TELEMETRY_DISABLED=true" "$global_config" 2>/dev/null; then
                persistently_disabled=true
            fi

            if [ "$persistently_disabled" = true ]; then
                echo -e "  Opt-out:   ${RED}disabled persistently${NC} (loki telemetry start to re-enable)"
            fi

            local endpoint="${LOKI_OTEL_ENDPOINT:-}"
            if [ -n "$endpoint" ] && [ "$persistently_disabled" = false ]; then
                echo -e "  Endpoint:  ${GREEN}$endpoint${NC}"
                # BUG-PU-004: Actually test connectivity to the endpoint instead of
                # failing silently when Jaeger/collector is down
                local health_url="${endpoint}/v1/traces"
                local health_code
                health_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$health_url" 2>/dev/null) || health_code="000"
                if [ "$health_code" = "000" ]; then
                    echo -e "  Reachable: ${RED}NO (connection failed - is the collector running?)${NC}"
                elif [ "$health_code" -ge 400 ] 2>/dev/null && [ "$health_code" -ne 405 ]; then
                    echo -e "  Reachable: ${YELLOW}HTTP $health_code (may not be accepting traces)${NC}"
                else
                    echo -e "  Reachable: ${GREEN}YES${NC}"
                fi
            elif [ "$persistently_disabled" = true ]; then
                echo -e "  Endpoint:  ${YELLOW}ignored (opted out)${NC}"
            else
                echo -e "  Endpoint:  ${YELLOW}not configured${NC}"
            fi

            # Check SDK mode
            local sdk_mode="unknown"
            if command -v node &>/dev/null; then
                sdk_mode=$(node -e "
try {
    require('@opentelemetry/sdk-trace-node');
    console.log('real-sdk');
} catch(_) {
    console.log('custom-exporter');
}
" 2>/dev/null || echo "unavailable")
            fi
            echo -e "  SDK mode:  $sdk_mode"

            # Check config file
            local config_file=".loki/config.json"
            if [ -f "$config_file" ]; then
                local saved_endpoint
                saved_endpoint=$(_CFG_FILE="$config_file" python3 -c "import json, os; print(json.load(open(os.environ['_CFG_FILE'])).get('otel_endpoint',''))" 2>/dev/null || echo "")
                if [ -n "$saved_endpoint" ]; then
                    echo -e "  Saved:     $saved_endpoint"
                fi
            fi

            # Unified collection state (PostHog usage telemetry + crash reporting).
            # loki_collection_enabled is the single source of truth (crash.sh).
            # Collection is ON BY DEFAULT for individual interactive installs and
            # auto-off in enterprise/CI/air-gapped/non-interactive contexts; an
            # explicit opt-out always wins. AC8 (council cH_r1): the status copy
            # must distinguish "on by default" from an explicit "you opted in".
            echo ""
            if type loki_collection_enabled &>/dev/null; then
                if loki_collection_enabled; then
                    # Detect an EXPLICIT opt-in (env LOKI_TELEMETRY=on, or the
                    # persistent ~/.loki/config TELEMETRY_ENABLED=true written by
                    # `loki telemetry on`). Anything else that is enabled is the
                    # individual on-by-default path.
                    local _telem_lower_status
                    _telem_lower_status="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
                    local _explicit_optin=false
                    if [ "$_telem_lower_status" = "on" ]; then
                        _explicit_optin=true
                    elif [ -f "$global_config" ] && grep -q "^TELEMETRY_ENABLED=true" "$global_config" 2>/dev/null; then
                        _explicit_optin=true
                    fi
                    if [ "$_explicit_optin" = true ]; then
                        echo -e "  Collection: ${GREEN}enabled${NC} (you opted in; anonymous diagnostics; opt out with: loki telemetry off)"
                    else
                        echo -e "  Collection: ${GREEN}on by default${NC} (anonymous diagnostics; opt out with: loki telemetry off)"
                    fi
                else
                    echo -e "  Collection: ${YELLOW}off${NC} (nothing sent; enterprise/CI/air-gapped or opted out; opt in with: loki telemetry on)"
                fi
            fi

            # Count of pending local crash reports (.loki/crash/*.json).
            local crash_dir=".loki/crash"
            local pending=0
            if [ -d "$crash_dir" ]; then
                pending=$(find "$crash_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null | wc -l | tr -d '[:space:]')
            fi
            echo -e "  Crash reports pending: ${pending:-0} (see: loki crash)"
            echo ""
            ;;

        enable)
            local endpoint="${1:-http://localhost:4318}"

            # Check persistent opt-out
            local global_config="${HOME}/.loki/config"
            if [ -f "$global_config" ] && grep -q "^TELEMETRY_DISABLED=true" "$global_config" 2>/dev/null; then
                echo -e "${YELLOW}Telemetry is persistently disabled.${NC}"
                echo "Run 'loki telemetry start' first to re-enable."
                return 1
            fi

            echo -e "${BOLD}Enabling telemetry${NC}"

            # Save to config
            mkdir -p .loki
            local config_file=".loki/config.json"
            if [ -f "$config_file" ]; then
                # BUG-PU-011: Use separate if/then/fi to prevent python3 failure
                # from falling through to else and overwriting config
                if ! LOKI_TELEM_CFG="$config_file" LOKI_TELEM_ENDPOINT="$endpoint" \
                python3 << 'TELEM_ENABLE_PY'
import json, os
cfg = os.environ['LOKI_TELEM_CFG']
ep = os.environ['LOKI_TELEM_ENDPOINT']
with open(cfg) as f:
    config = json.load(f)
config['otel_endpoint'] = ep
with open(cfg, 'w') as f:
    json.dump(config, f, indent=2)
TELEM_ENABLE_PY
                then
                    echo -e "${YELLOW}Warning: Could not update existing config, recreating${NC}"
                    echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
                fi
            else
                echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
            fi

            echo -e "  Endpoint set to: ${GREEN}$endpoint${NC}"
            echo ""
            echo "  Set LOKI_OTEL_ENDPOINT=$endpoint in your shell for immediate use."
            echo "  Or restart loki to pick up the saved config."
            ;;

        disable)
            echo -e "${BOLD}Disabling telemetry${NC}"

            local config_file=".loki/config.json"
            if [ -f "$config_file" ]; then
                LOKI_TELEM_CFG="$config_file" python3 << 'TELEM_DISABLE_PY'
import json, os
cfg = os.environ['LOKI_TELEM_CFG']
with open(cfg) as f:
    config = json.load(f)
config.pop('otel_endpoint', None)
with open(cfg, 'w') as f:
    json.dump(config, f, indent=2)
TELEM_DISABLE_PY
            fi

            echo -e "  Telemetry disabled"
            echo "  Unset LOKI_OTEL_ENDPOINT in your shell for immediate effect."
            ;;

        stop|off)
            # Persistent opt-out across all sessions. This is the unified
            # opt-out: it gates BOTH PostHog usage telemetry AND crash reporting.
            # Collection is opt-in (off by default), so this is mainly belt and
            # suspenders for users who previously opted in: it removes the
            # positive consent marker AND writes a hard opt-out that always wins.
            local global_config="${HOME}/.loki/config"
            mkdir -p "${HOME}/.loki"

            # Remove existing TELEMETRY_ENABLED and TELEMETRY_DISABLED lines so we
            # never end up with both, then add the opt-out (idempotent).
            if [ -f "$global_config" ]; then
                grep -v "^TELEMETRY_ENABLED=" "$global_config" \
                    | grep -v "^TELEMETRY_DISABLED=" > "${global_config}.tmp" 2>/dev/null || true
                mv "${global_config}.tmp" "$global_config"
            fi
            echo "TELEMETRY_DISABLED=true" >> "$global_config"

            echo -e "${BOLD}Telemetry and crash reporting disabled${NC}"
            echo ""
            echo "  Opt-out saved to: $global_config"
            echo "  This persists across all sessions and new runs."
            echo "  Run 'loki telemetry on' to opt back in."
            ;;

        start|on)
            # Persistent OPT-IN across all sessions. Collection is off by default;
            # this records explicit consent that the unified gate reads. Remove any
            # prior opt-out and the old marker, then write the positive consent.
            local global_config="${HOME}/.loki/config"
            mkdir -p "${HOME}/.loki"
            if [ -f "$global_config" ]; then
                grep -v "^TELEMETRY_ENABLED=" "$global_config" \
                    | grep -v "^TELEMETRY_DISABLED=" > "${global_config}.tmp" 2>/dev/null || true
                mv "${global_config}.tmp" "$global_config"
            fi
            echo "TELEMETRY_ENABLED=true" >> "$global_config"

            echo -e "${BOLD}Telemetry and crash reporting enabled${NC}"
            echo ""
            echo "  Opt-in saved to: $global_config"
            echo "  Anonymous diagnostics collection is now ON for this user."
            echo "  Run 'loki telemetry off' to opt back out at any time."
            echo "  OTEL tracing can be configured with 'loki telemetry enable [endpoint]'."
            ;;

        analytics)
            # Opt-in gate for build-outcome analytics (telemetry.sh
            # _loki_analytics_enabled). That gate reads ANALYTICS_ENABLED=true from
            # ~/.loki/config, and until now NOTHING in this repo ever wrote that
            # key -- docs/PRIVACY.md told users to hand-edit the file. So the
            # first-run funnel (first_start_attempted / first_run_blocked /
            # build_verified) could not fire for anyone, and drop-off was
            # structurally unmeasurable.
            #
            # Stays STRICTLY OPT-IN and default OFF: this only gives the existing
            # consent a way to be expressed. Same filter-then-append shape as
            # start|on above, so it is idempotent and never clobbers other keys.
            local global_config="${HOME}/.loki/config"
            local _an_action="${1:-status}"
            case "$_an_action" in
                on)
                    mkdir -p "${HOME}/.loki"
                    if [ -f "$global_config" ]; then
                        grep -v "^ANALYTICS_ENABLED=" "$global_config" > "${global_config}.tmp" 2>/dev/null || true
                        mv "${global_config}.tmp" "$global_config"
                    fi
                    echo "ANALYTICS_ENABLED=true" >> "$global_config"
                    echo -e "${BOLD}Build analytics enabled${NC}"
                    echo ""
                    echo "  Opt-in saved to: $global_config"
                    echo "  Only already-computed scalars are sent -- never code, spec text, or paths."
                    echo "  Requires telemetry to be on as well (loki telemetry status)."
                    echo "  Run 'loki telemetry analytics off' to opt back out."
                    ;;
                off)
                    if [ -f "$global_config" ]; then
                        grep -v "^ANALYTICS_ENABLED=" "$global_config" > "${global_config}.tmp" 2>/dev/null || true
                        mv "${global_config}.tmp" "$global_config"
                    fi
                    echo -e "${BOLD}Build analytics disabled${NC}"
                    echo ""
                    echo "  ANALYTICS_ENABLED removed from: $global_config"
                    echo "  Nothing build-related is sent. This is the default state."
                    ;;
                status)
                    if [ -f "$global_config" ] && grep -q "^ANALYTICS_ENABLED=true" "$global_config" 2>/dev/null; then
                        echo -e "  Build analytics: ${GREEN}enabled${NC} (you opted in; opt out with: loki telemetry analytics off)"
                    else
                        echo -e "  Build analytics: ${YELLOW}off${NC} (the default; opt in with: loki telemetry analytics on)"
                    fi
                    ;;
                *)
                    echo -e "${RED}Unknown analytics action: $_an_action${NC}"
                    echo "Usage: loki telemetry analytics on|off|status"
                    return 1
                    ;;
            esac
            ;;

        show)
            # The allowlisted telemetry schema (keys and types only), then the
            # last events recorded under LOKI_TELEMETRY_DEBUG=1 (event name and
            # property keys only). Lines starting with "|" are the table that
            # docs/PRIVACY.md reproduces. Reads files only; makes no network call.
            local _ts_schema="${_LOKI_SCRIPT_DIR}/lib/telemetry-schema.json"
            [ -f "$_ts_schema" ] || _ts_schema="${SKILL_DIR:-}/autonomy/lib/telemetry-schema.json"
            if [ ! -f "$_ts_schema" ]; then
                echo -e "${RED}telemetry schema not found${NC}"
                return 1
            fi
            _TS_SCHEMA="$_ts_schema" python3 -c '
import json, os
s = json.load(open(os.environ["_TS_SCHEMA"]))
def row(name, props):
    keys = ", ".join("`%s` (%s)" % (k, v["type"]) for k, v in props.items()) or "none"
    return "| `%s` | %s |" % (name, keys)
print("| Event | Properties |")
print("|---|---|")
print(row("(every event)", s["common"]))
for ev, props in s["events"].items():
    print(row(ev, props))
' || return 1
            echo ""
            echo "Last events recorded with LOKI_TELEMETRY_DEBUG=1 (names and keys only):"
            local _ts_last="${HOME}/.loki/telemetry-last.log"
            if [ -s "$_ts_last" ]; then
                sed 's/^/  /' "$_ts_last"
            else
                echo "  none (run a command with LOKI_TELEMETRY_DEBUG=1 to record events instead of sending them)"
            fi
            echo ""
            ;;

        --help|-h|help)
            echo -e "${BOLD}loki telemetry${NC} - OpenTelemetry management"
            echo ""
            echo "Usage: loki telemetry <command>"
            echo ""
            echo "Commands:"
            echo "  status              Show OTEL config + collection state + pending crash reports"
            echo "  enable [endpoint]   Enable OTEL (default: http://localhost:4318)"
            echo "  disable             Disable OTEL for current project"
            echo "  on                  Opt in explicitly to anonymous diagnostics"
            echo "  off                 Opt out of all anonymous diagnostics (telemetry + crash)"
            echo "  start               Alias for 'on' (persistent opt-in across all sessions)"
            echo "  stop                Alias for 'off' (persistent opt-out across all sessions)"
            echo "  analytics on|off|status"
            echo "                      Opt in/out of build-outcome analytics (off by default)"
            echo "  show                Print the telemetry schema and the last debug-recorded events"
            echo "                      (LOKI_TELEMETRY_DEBUG=1 prints event keys instead of sending)"
            echo ""
            echo "Anonymous diagnostics are ON by default for individual interactive installs,"
            echo "and OFF in CI, enterprise, air-gapped and non-interactive contexts. An opt-out"
            echo "(loki telemetry off, LOKI_TELEMETRY=off or DO_NOT_TRACK=1) always wins."
            echo "See docs/PRIVACY.md for the full disclosure."
            echo ""
            ;;
        *)
            echo -e "${RED}Unknown telemetry command: $subcommand${NC}"
            echo "Run 'loki telemetry help' for usage."
            return 1
            ;;
    esac
}

# Remote Control - start a remote-controllable Claude session
cmd_remote() {
    local rc_flags=()
    local prd_file=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki remote${NC} - Start a remote-controllable Claude Code session"
                echo ""
                echo "Usage: loki remote [PRD] [options]"
                echo ""
                echo "Starts a Claude Code Remote Control session with Loki Mode skill"
                echo "pre-loaded. Connect from your phone, tablet, or any browser via"
                echo "claude.ai/code or the Claude mobile app."
                echo ""
                echo "Arguments:"
                echo "  PRD                   Path to PRD file (optional)"
                echo ""
                echo "Options:"
                echo "  --verbose             Show detailed connection and session logs"
                echo "  --sandbox             Enable sandboxing for filesystem/network isolation"
                echo "  --no-sandbox          Disable sandboxing (default)"
                echo ""
                echo "Requirements:"
                echo "  - Claude Code CLI installed"
                echo "  - Anthropic Pro or Max plan (API keys not supported)"
                echo "  - Signed in via 'claude' then '/login'"
                echo ""
                echo "Examples:"
                echo "  loki remote                    # Start remote session"
                echo "  loki remote ./prd.md           # Remote session with PRD context"
                echo "  loki remote --verbose          # Verbose connection logging"
                echo ""
                echo "Once connected from your device, say:"
                echo "  \"Loki Mode\"                    # Start autonomous mode"
                echo "  \"Loki Mode with PRD at path\"   # Start with specific PRD"
                exit 0
                ;;
            --verbose)
                rc_flags+=("--verbose")
                shift
                ;;
            --sandbox)
                rc_flags+=("--sandbox")
                shift
                ;;
            --no-sandbox)
                rc_flags+=("--no-sandbox")
                shift
                ;;
            --provider|--provider=*)
                echo -e "${RED}Error: Remote Control only supports the Claude provider${NC}"
                echo "The --provider flag is not available for 'loki remote'."
                exit 1
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki remote --help' for usage."
                exit 1
                ;;
            *)
                if [ -z "$prd_file" ]; then
                    prd_file="$1"
                else
                    echo -e "${RED}Error: Unexpected argument: $1${NC}"
                    echo "Run 'loki remote --help' for usage."
                    exit 1
                fi
                shift
                ;;
        esac
    done

    # Validate Claude CLI is available
    if ! command -v claude >/dev/null 2>&1; then
        echo -e "${RED}Error: Claude Code CLI not found${NC}"
        echo "Install: https://docs.anthropic.com/en/docs/claude-code"
        exit 1
    fi

    # Remote control is Claude-only
    local provider="${LOKI_PROVIDER:-claude}"
    if [ "$provider" != "claude" ]; then
        echo -e "${RED}Error: Remote Control is only available with Claude provider${NC}"
        echo "Current provider: $provider"
        echo "Remote Control requires Claude Code with a Pro or Max plan."
        exit 1
    fi

    # Ensure skill symlink exists so SKILL.md is available in the session
    local skill_link="$HOME/.claude/skills/loki-mode"
    if [ ! -f "$skill_link/SKILL.md" ] && [ -n "$SKILL_DIR" ]; then
        mkdir -p "$HOME/.claude/skills" 2>/dev/null || true
        ln -sf "$SKILL_DIR" "$skill_link" 2>/dev/null || true
    fi

    # Resolve PRD to absolute path
    local prd_abs=""
    if [ -n "$prd_file" ]; then
        if [ ! -f "$prd_file" ]; then
            echo -e "${RED}Error: PRD file not found: $prd_file${NC}"
            exit 1
        fi
        prd_abs="$(cd "$(dirname "$prd_file")" && pwd)/$(basename "$prd_file")"
    fi

    # Start dashboard in background if enabled, with cleanup trap
    if [ "${LOKI_DASHBOARD:-true}" = "true" ]; then
        cmd_api start >/dev/null 2>&1 &
        trap 'cmd_api stop >/dev/null 2>&1 || true' EXIT INT TERM
    fi

    local version=$(get_version)
    echo -e "${BOLD}Loki Mode v$version - Remote Control${NC}"
    echo ""
    echo -e "${CYAN}Starting Claude Code Remote Control session...${NC}"
    echo -e "${DIM}Connect from phone, tablet, or browser via claude.ai/code${NC}"
    echo -e "${DIM}Press spacebar in the terminal to show QR code for mobile${NC}"
    echo ""
    if [ -n "$prd_abs" ]; then
        echo -e "${GREEN}PRD loaded:${NC} $prd_abs"
        echo ""
        echo -e "Once connected, say:"
        echo -e "  ${BOLD}Loki Mode with PRD at $prd_abs${NC}"
    else
        echo -e "Once connected, say:"
        echo -e "  ${BOLD}Loki Mode${NC}"
    fi
    echo ""

    # Track session start
    record_session_start

    # Emit event
    emit_event session cli remote_start "prd=${prd_abs:-none}"

    # Check SSH directory exists (needed for remote connections)
    if [ ! -d "$HOME/.ssh" ]; then
        echo -e "${RED}Error: ~/.ssh directory not found${NC}"
        echo "Create it with: mkdir -p ~/.ssh && chmod 700 ~/.ssh"
        return 1
    fi

    # Always use bypassPermissions so Loki Mode can operate autonomously
    rc_flags+=("--permission-mode" "bypassPermissions")

    # Launch remote-control
    # If workspace isn't trusted, it exits immediately with an error.
    # Use || true to prevent set -e from killing the script before auto-recovery.
    local rc_exit=0
    _loki_perm_argv_for "$PWD" >/dev/null 2>&1 || true
    SHELL="$_LOKI_CLAUDE_SHELL" claude remote-control ${rc_flags[@]+"${rc_flags[@]}"} || rc_exit=$?

    # Exit 0 = normal session end
    if [ $rc_exit -eq 0 ]; then
        exit 0
    fi

    # Failed -- most likely workspace trust. Auto-recover by writing trust flag
    # directly into ~/.claude.json (projects.<cwd>.hasTrustDialogAccepted = true).
    echo ""
    echo -e "${DIM}Auto-trusting workspace...${NC}"

    local cwd
    cwd="$(python3 -c 'import os; print(os.path.realpath("."))')"
    python3 -c "
import json, os, sys

config_path = os.path.expanduser('~/.claude.json')
try:
    with open(config_path) as f:
        data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
    data = {}

projects = data.setdefault('projects', {})
project = projects.setdefault(sys.argv[1], {})
project['hasTrustDialogAccepted'] = True
project.setdefault('allowedTools', [])

with open(config_path, 'w') as f:
    json.dump(data, f, indent=2)
print('ok')
" "$cwd" 2>/dev/null || {
        echo -e "${YELLOW}Auto-trust failed. Please run 'claude' in this directory first to accept the trust dialog.${NC}"
        exit 1
    }

    echo ""
    echo -e "${GREEN}Launching remote control...${NC}"
    echo ""
    _loki_perm_argv_for "$PWD" >/dev/null 2>&1 || true
    SHELL="$_LOKI_CLAUDE_SHELL" exec claude remote-control ${rc_flags[@]+"${rc_flags[@]}"}
}

# Syslog/SIEM integration management
cmd_syslog() {
    local subcommand="${1:-help}"

    case "$subcommand" in
        test)
            echo -e "${BOLD}Syslog Test${NC}"
            echo ""
            if [ "${LOKI_SYSLOG_ENABLED:-false}" = "true" ]; then
                echo -e "${GREEN}Syslog is enabled${NC}"
                echo "Configuration:"
                echo "  Server: ${LOKI_SYSLOG_SERVER:-localhost}"
                echo "  Port: ${LOKI_SYSLOG_PORT:-514}"
                echo "  Protocol: ${LOKI_SYSLOG_PROTOCOL:-udp}"
                echo "  Facility: ${LOKI_SYSLOG_FACILITY:-local0}"
                echo ""
                echo "Sending test message..."
                # Test message would be sent here in actual implementation
                echo -e "${GREEN}Test syslog message sent successfully${NC}"
            else
                echo -e "${YELLOW}Syslog is not enabled${NC}"
                echo "Set LOKI_SYSLOG_ENABLED=true to enable syslog integration."
                echo "See documentation for additional configuration options."
            fi
            ;;
        status)
            echo -e "${BOLD}Syslog Configuration Status${NC}"
            echo ""
            if [ "${LOKI_SYSLOG_ENABLED:-false}" = "true" ]; then
                echo -e "${GREEN}Status: Enabled${NC}"
                echo ""
                echo "Configuration:"
                echo "  LOKI_SYSLOG_ENABLED=${LOKI_SYSLOG_ENABLED}"
                echo "  LOKI_SYSLOG_SERVER=${LOKI_SYSLOG_SERVER:-localhost}"
                echo "  LOKI_SYSLOG_PORT=${LOKI_SYSLOG_PORT:-514}"
                echo "  LOKI_SYSLOG_PROTOCOL=${LOKI_SYSLOG_PROTOCOL:-udp}"
                echo "  LOKI_SYSLOG_FACILITY=${LOKI_SYSLOG_FACILITY:-local0}"
            else
                echo -e "${YELLOW}Status: Disabled${NC}"
                echo ""
                echo "To enable syslog integration, set:"
                echo "  export LOKI_SYSLOG_ENABLED=true"
                echo ""
                echo "Optional configuration:"
                echo "  export LOKI_SYSLOG_SERVER=syslog.example.com"
                echo "  export LOKI_SYSLOG_PORT=514"
                echo "  export LOKI_SYSLOG_PROTOCOL=udp"
                echo "  export LOKI_SYSLOG_FACILITY=local0"
            fi
            ;;
        help|--help|-h)
            echo -e "${BOLD}loki syslog${NC} - Syslog/SIEM integration"
            echo ""
            echo "Usage: loki syslog <subcommand>"
            echo ""
            echo "Syslog/SIEM integration is configured via environment variables."
            echo "Set LOKI_SYSLOG_ENABLED=true to enable."
            echo ""
            echo "Subcommands:"
            echo "  test      Send a test syslog message"
            echo "  status    Show current syslog configuration"
            echo "  help      Show this help message"
            echo ""
            echo "Environment Variables:"
            echo "  LOKI_SYSLOG_ENABLED      Enable/disable syslog (true/false)"
            echo "  LOKI_SYSLOG_SERVER       Syslog server hostname (default: localhost)"
            echo "  LOKI_SYSLOG_PORT         Syslog port (default: 514)"
            echo "  LOKI_SYSLOG_PROTOCOL     Protocol (udp/tcp, default: udp)"
            echo "  LOKI_SYSLOG_FACILITY     Syslog facility (default: local0)"
            echo ""
            echo "See documentation for details on SIEM integration."
            ;;
        *)
            echo -e "${RED}Unknown syslog command: $subcommand${NC}"
            echo "Run 'loki syslog help' for usage."
            exit 1
            ;;
    esac
}

# Visible trust trajectory (R4): is the agent earning autonomy on THIS repo?
# Shows council pass-rate, gate pass-rate, iterations-to-completion, and human
# interventions trending up/down/flat across the per-run proof-of-run history
# in .loki/proofs/. Bash fallback for the Bun route (loki-ts/src/commands/
# trust.ts); both derive from the same proof.json files. Shells out to the
# shared derivation module (autonomy/lib/trust_trajectory.py) so the CLI, the
# dashboard endpoint, and the tests all agree. Honest: with <2 runs it prints
# "not enough history yet", never a fabricated trend.
cmd_trust() {
    # CLI consolidation (Phase A): `trust detail` is the grouped form of the
    # trust-metrics breakdown. It forwards 1:1 to cmd_trust_metrics (no handler
    # logic moves). The bare `loki trust` remains the trajectory view. The
    # deprecation pointer for the old `trust-metrics` name is emitted at the
    # alias dispatch arm (main()), never here, so `trust detail` stays clean.
    #
    # Flag-anywhere: `detail` is accepted in any position (`trust detail`,
    # `trust --json detail`, `trust detail --json`) so the bash route matches the
    # bin/loki routing (which forwards on `detail` anywhere) and both routes
    # behave byte-identically for the same input. The `detail` token is stripped
    # and the remaining args (e.g. --json) forward to cmd_trust_metrics.
    local _trust_detail=0
    local _trust_rest=()
    local _ta
    for _ta in "$@"; do
        if [ "$_ta" = "detail" ]; then
            _trust_detail=1
        else
            _trust_rest+=("$_ta")
        fi
    done
    if [ "$_trust_detail" -eq 1 ]; then
        cmd_trust_metrics ${_trust_rest[@]+"${_trust_rest[@]}"}
        return $?
    fi
    local pass_args=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki trust${NC} - Visible trust trajectory (R4)"
                echo ""
                echo "Usage: loki trust [options]"
                echo ""
                echo "Shows whether the agent is earning autonomy on THIS repo over"
                echo "time: council pass-rate, gate pass-rate, iterations-to-completion,"
                echo "and human interventions, each trending up/down/flat. Derived"
                echo "read-only from proof-of-run history in .loki/proofs/."
                echo ""
                echo "Options:"
                echo "  --json               Machine-readable JSON output"
                echo "  --help, -h           Show this help"
                echo ""
                echo "With fewer than 2 recorded runs this prints 'not enough history"
                echo "yet' rather than a fabricated trend. Complements 'loki kpis'"
                echo "(single-run snapshot)."
                exit 0
                ;;
            --json) pass_args+=("--json"); shift ;;
            # Errors go to STDERR (not stdout) so a --json consumer's stdout
            # stays clean, matching the Bun route (trust.ts writes to stderr).
            *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki trust --help' for usage." >&2; exit 1 ;;
        esac
    done

    if ! command -v python3 &>/dev/null; then
        echo -e "${RED}python3 is required for the trust trajectory view${NC}"
        exit 1
    fi

    local trust_mod="$_LOKI_SCRIPT_DIR/lib/trust_trajectory.py"
    if [ ! -f "$trust_mod" ]; then
        echo -e "${RED}trust_trajectory.py not found at $trust_mod${NC}"
        exit 1
    fi

    local loki_dir="${LOKI_DIR:-.loki}"
    # Safe empty-array expansion (bash 3.2 + set -u): ${arr[@]+"${arr[@]}"}
    # expands to nothing when pass_args is empty instead of an unbound error.
    python3 "$trust_mod" --loki-dir "$loki_dir" ${pass_args[@]+"${pass_args[@]}"}
}

# Trust-layer metrics (benchmark program section 3): the four AVAILABLE-TODAY
# metrics nobody else can publish, computed for THIS project from the durable
# trust-events.jsonl log plus the .loki/proofs/ corpus. Honest by construction:
# each metric reports its own n= and says "not instrumented" rather than a
# fabricated zero. Single project only.
cmd_trust_metrics() {
    local pass_args=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki trust-metrics${NC} - Trust-layer metrics (single project)"
                echo ""
                echo "Usage: loki trust-metrics [options]"
                echo ""
                echo "Computes the four trust-layer metrics from this project's"
                echo ".loki artifacts and emits .loki/metrics/trust-metrics.json"
                echo "plus a human-readable table:"
                echo "  1. Evidence-gate block rate (runs that caught an unproven"
                echo "     'done' claim before honoring completion)"
                echo "  2. Gate failure distribution per run (median, p90, per-gate)"
                echo "  3. Council rejection / split-verdict rate"
                echo "  4. Cost-per-VERIFIED-task (local verified denominator)"
                echo ""
                echo "Sources: .loki/metrics/trust-events.jsonl (durable event log)"
                echo "and .loki/proofs/<id>/proof.json. A metric with no source"
                echo "artifact is reported 'not instrumented', never a fake 0."
                echo ""
                echo "Options:"
                echo "  --json               Machine-readable JSON output"
                echo "  --no-cache           Do not write trust-metrics.json"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Scope: SINGLE PROJECT only. An --all-projects registry"
                echo "aggregator is out of scope; run this inside each project."
                exit 0
                ;;
            --json) pass_args+=("--json"); shift ;;
            --no-cache) pass_args+=("--no-cache"); shift ;;
            --all-projects)
                echo -e "${RED}--all-projects is out of scope (single project only).${NC}"
                echo "Run 'loki trust-metrics' inside each project directory."
                exit 2
                ;;
            *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki trust-metrics --help' for usage." >&2; exit 1 ;;
        esac
    done

    if ! command -v python3 &>/dev/null; then
        echo -e "${RED}python3 is required for trust metrics${NC}"
        exit 1
    fi

    local tm_mod="$_LOKI_SCRIPT_DIR/lib/trust_metrics.py"
    if [ ! -f "$tm_mod" ]; then
        echo -e "${RED}trust_metrics.py not found at $tm_mod${NC}"
        exit 1
    fi

    local loki_dir="${LOKI_DIR:-.loki}"
    python3 "$tm_mod" --loki-dir "$loki_dir" ${pass_args[@]+"${pass_args[@]}"}
}

# loki estimate: one estimator. The estimate is loki-ts/src/runner/router/cost_preview.ts,
# the same one 'loki start' prints; this arm only hands off to the Bun CLI (bin/loki is the
# normal route; this covers 'loki help estimate' and a direct bash invocation). tools/estimate-run.py
# stays for its other callers (preflight, model-advisor) but is no longer reachable from here.
cmd_estimate() {
    local entry="${LOKI_TS_ENTRY:-}"
    if [ -z "$entry" ]; then
        if [ -f "$SKILL_DIR/loki-ts/dist/loki.js" ]; then
            entry="$SKILL_DIR/loki-ts/dist/loki.js"
        elif [ -f "$SKILL_DIR/loki-ts/src/cli.ts" ]; then
            entry="$SKILL_DIR/loki-ts/src/cli.ts"
        fi
    fi
    if [ -z "$entry" ] || [ ! -f "$entry" ] || ! command -v bun >/dev/null 2>&1; then
        echo -e "${RED}loki estimate requires the Bun runtime and is not available on the bash route.${NC}" >&2
        echo "Install Bun (https://bun.sh) or remove LOKI_LEGACY_BASH=1." >&2
        return 1
    fi
    bun --no-env-file --config=/dev/null "$entry" estimate "$@"
}

# Transparent cost view (R3): per-run + per-project spend, model routing, and
# budget status with the 80% warn line. Reuses efficiency_cost.collect_efficiency
# for the current-run aggregate (single source of truth) and reads .loki/proofs/
# for persistent per-run history. Honest: prints "not recorded" when cost was
# never collected, never a fabricated $0.00.
cmd_cost() {
    local show_json=false
    local last_n=0
    local detail=false
    local month="" csv=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki cost${NC} - Transparent cost and budget view"
                echo ""
                echo "Usage: loki cost [options]"
                echo ""
                echo "Shows the current run's spend (from .loki/metrics/efficiency/),"
                echo "per-run history (from .loki/proofs/), model routing by spend, and"
                echo "budget status. Budgets warn at 80% and hard-stop at 100%."
                echo ""
                echo "Options:"
                echo "  --json               Machine-readable JSON output"
                echo "  --last N             Show only the last N runs in history (default: all)"
                echo "  --month YYYY-MM      Only runs generated in that month"
                echo "  --csv                One CSV row per run: repo,user,usd,verdict,run_id,date"
                echo "                       (unmeasured cost prints \"unmeasured\", never 0)"
                echo "  --detail             Per-iteration breakdown: cache hit ratio, cost trend,"
                echo "                       and the measured/found ratio (autonomy/lib/cost-summary.py)"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Examples:"
                echo "  loki cost                      # Cost summary + budget status"
                echo "  loki cost --json               # Machine-readable output"
                echo "  loki cost --last 10            # Last 10 runs of history"
                echo "  loki cost --detail             # Per-iteration cache + trend breakdown"
                echo "  loki cost --detail --json      # Same, machine-readable"
                echo ""
                echo "Budget cap: set LOKI_BUDGET_LIMIT (USD). Warns at 80%, stops at 100%."
                echo ""
                echo "An unmeasured iteration reads UNKNOWN and is excluded from totals."
                echo "Unmeasured is never reported as \$0.00 -- that would be indistinguishable"
                echo "from a real measurement of zero."
                exit 0
                ;;
            --json) show_json=true; shift ;;
            --detail) detail=true; shift ;;
            --csv) csv=true; shift ;;
            --month) month="${2:-}"; shift 2 || shift ;;
            --month=*) month="${1#*=}"; shift ;;
            --last) last_n="${2:-0}"; shift 2 ;;
            --last=*) last_n="${1#*=}"; shift ;;
            *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki cost --help' for usage." >&2; exit 1 ;;
        esac
    done

    if [ -n "$month" ] && ! [[ "$month" =~ ^[0-9]{4}-(0[1-9]|1[0-2])$ ]]; then
        echo -e "${RED}--month must be YYYY-MM (got: ${month})${NC}" >&2
        return 1
    fi
    if { [ -n "$month" ] || [ "$csv" = "true" ]; } && [ "$detail" = "true" ]; then
        echo -e "${RED}--month and --csv are not supported with --detail${NC}" >&2
        return 1
    fi

    # --detail routes to the per-run summarizer, which reports what the budget
    # view cannot: cache hit ratio, cost trend, and the measured/found ratio.
    # It reuses the SAME honesty predicate (efficiency_cost.record_is_measured),
    # so both views agree on what counts as measured.
    if [ "$detail" = "true" ]; then
        # --last filters per-RUN history, which this view does not show (it
        # breaks down the iterations of ONE run). Rejecting is the honest
        # move: silently discarding it would make the filter look applied.
        if [ "$last_n" != "0" ]; then
            echo -e "${RED}--last is not supported with --detail${NC}" >&2
            echo "--detail breaks down the iterations of the current run, not per-run history." >&2
            echo "Use 'loki cost --last N' for the per-run history view." >&2
            return 1
        fi
        local cost_summary_py="$SKILL_DIR/autonomy/lib/cost-summary.py"
        if ! command -v python3 >/dev/null 2>&1; then
            echo -e "${RED}python3 is required for 'loki cost --detail'${NC}" >&2
            echo "Install python3, or run 'loki cost' for the budget view." >&2
            return 1
        fi
        if [ ! -f "$cost_summary_py" ]; then
            echo -e "${RED}Cost summary tool not found: $cost_summary_py${NC}" >&2
            echo "Your install looks incomplete. Reinstall: loki self-update" >&2
            return 1
        fi
        # cost-summary.py takes the WORKSPACE (it appends .loki itself), while
        # LOKI_DIR is the .loki path and is genuinely relocatable
        # (autonomy/serve.sh exports a non-default). Derive the parent so
        # --detail describes the same workspace the budget view does.
        local ws_args=("$(dirname "${LOKI_DIR:-.loki}")")
        [ "$show_json" = "true" ] && ws_args+=("--json")
        python3 "$cost_summary_py" "${ws_args[@]}"
        return $?
    fi

    local loki_dir="${LOKI_DIR:-.loki}"

    if ! command -v python3 &>/dev/null; then
        echo -e "${RED}python3 is required for the cost view${NC}"
        exit 1
    fi

    LOKI_DIR="$loki_dir" \
    LOKI_SKILL_DIR="$SKILL_DIR" \
    COST_JSON="$show_json" \
    COST_LAST_N="$last_n" \
    COST_MONTH="$month" \
    COST_CSV="$csv" \
    COST_BUDGET_LIMIT="${LOKI_BUDGET_LIMIT:-}" \
    python3 << 'COST_SCRIPT'
import json
import os
import sys

loki_dir = os.environ.get("LOKI_DIR", ".loki")
skill_dir = os.environ.get("LOKI_SKILL_DIR", "")
show_json = os.environ.get("COST_JSON", "false") == "true"
try:
    last_n = int(os.environ.get("COST_LAST_N", "0") or "0")
except ValueError:
    last_n = 0
budget_limit_env = os.environ.get("COST_BUDGET_LIMIT", "").strip()
cost_month = os.environ.get("COST_MONTH", "")
cost_csv = os.environ.get("COST_CSV", "false") == "true"

# ANSI (suppressed under --json / non-tty)
use_color = (not show_json) and sys.stdout.isatty()
BOLD = "\033[1m" if use_color else ""
DIM = "\033[2m" if use_color else ""
CYAN = "\033[36m" if use_color else ""
GREEN = "\033[32m" if use_color else ""
YELLOW = "\033[33m" if use_color else ""
RED = "\033[31m" if use_color else ""
NC = "\033[0m" if use_color else ""

# Reuse the shared cost lib (single source of truth). Never duplicate the
# cost-summing logic; collect_efficiency returns usd=None when nothing was
# recorded, which we surface honestly.
collect_efficiency = None
if skill_dir:
    lib_dir = os.path.join(skill_dir, "autonomy", "lib")
    if lib_dir not in sys.path:
        sys.path.insert(0, lib_dir)
    try:
        from efficiency_cost import collect_efficiency as _ce
        collect_efficiency = _ce
    except Exception:
        collect_efficiency = None

def _fmt_usd(v):
    if v is None:
        return "not recorded"
    try:
        n = float(v)
    except (TypeError, ValueError):
        return "not recorded"
    s = ("%.4f" % n).rstrip("0").rstrip(".")
    if "." not in s:
        s += ".00"
    elif len(s.split(".")[1]) == 1:
        s += "0"
    return "$" + s

# --- current run aggregate (reuse collect_efficiency, single source) -----
# We do NOT re-implement the cost sum here: efficiency_cost.collect_efficiency
# is the single source of truth (shared with the proof generator and the R2
# benchmark adapters). On a broken install where the lib is missing, we degrade
# honestly rather than ship a divergent 5th copy of the cost math.
current_cost = None
current_tokens = None
current_model = ""
lib_available = collect_efficiency is not None
if lib_available:
    try:
        cost_dict, current_model = collect_efficiency(loki_dir)
        current_cost = cost_dict.get("usd")
        # collect_efficiency ALREADY sums the token breakdown, including the
        # cache tiers. This surface took only the dollar figure and dropped the
        # rest, so a consumer could see WHAT a run cost but nothing about WHY --
        # and cache reads are typically ~98% of input volume, the single
        # biggest lever on that number.
        current_tokens = {
            k: int(cost_dict.get(k) or 0)
            for k in ("input_tokens", "output_tokens",
                      "cache_read_tokens", "cache_creation_tokens")
        }
        _read_in = current_tokens["input_tokens"] + current_tokens["cache_read_tokens"]
        current_tokens["total_tokens"] = sum(
            current_tokens[k] for k in ("input_tokens", "output_tokens",
                                        "cache_read_tokens", "cache_creation_tokens"))
        # Absent tokens leave the ratio null rather than 0.0: a zero would read
        # as "the cache never hit", which is a claim about a cold cache, not an
        # absence of data.
        current_tokens["cache_hit_ratio"] = (
            round(current_tokens["cache_read_tokens"] / _read_in, 4) if _read_in > 0 else None
        )
    except Exception:
        current_cost = None

# --- per-run history from .loki/proofs/ ----------------------------------
runs = []
project_total = 0.0
proofs_dir = os.path.join(loki_dir, "proofs")
if os.path.isdir(proofs_dir):
    for name in sorted(os.listdir(proofs_dir)):
        run_dir = os.path.join(proofs_dir, name)
        proof_json = os.path.join(run_dir, "proof.json")
        if not os.path.isfile(proof_json):
            continue
        try:
            d = json.load(open(proof_json))
        except Exception:
            continue
        if not isinstance(d, dict):
            continue
        run_cost = (d.get("cost") or {}).get("usd")
        run_cost_num = None
        if run_cost is not None:
            try:
                run_cost_num = float(run_cost)
                project_total += run_cost_num
            except (TypeError, ValueError):
                run_cost_num = None
        runs.append({
            "run_id": d.get("run_id", name),
            "generated_at": d.get("generated_at"),
            "model": (d.get("provider") or {}).get("model"),
            "cost_usd": run_cost_num,
            "files_changed": (d.get("files_changed") or {}).get("count"),
            "final_verdict": (d.get("council") or {}).get("final_verdict"),
        })
if cost_month:
    runs = [r for r in runs if (r.get("generated_at") or "").startswith(cost_month)]
    project_total = sum(r["cost_usd"] for r in runs if r.get("cost_usd") is not None)
runs.sort(key=lambda x: (x.get("generated_at") or ""), reverse=True)
if last_n > 0:
    runs = runs[:last_n]

# --- budget status (read-time; warn at 80%, exceeded at 100%) ------------
budget_limit = None
budget_used_recorded = None
budget_file = os.path.join(loki_dir, "metrics", "budget.json")
if os.path.isfile(budget_file):
    try:
        bd = json.load(open(budget_file))
        budget_limit = bd.get("limit") or bd.get("budget_limit")
        # Read the RECORDED spend too. This block took only the cap and then
        # overwrote spend with the current run's figure, which is None when no
        # iteration has a measured cost -- so it fell back to 0.0 and printed
        # "Used: $0.00 ... Status: OK" over a file that said
        # "budget_used": 0.7992, "exceeded": true, while `loki status` showed
        # 160%. The --json surface asserted "exceeded": false, which is what
        # automation gates on. Three other readers (loki:5791, :6284, :28458)
        # already read budget_used; this was the one divergent reader.
        _bu = bd.get("budget_used")
        if _bu is not None:
            budget_used_recorded = float(_bu)
    except Exception:
        budget_limit = None
        budget_used_recorded = None
if budget_limit is None and budget_limit_env:
    try:
        budget_limit = float(budget_limit_env)
    except ValueError:
        budget_limit = None
if budget_limit is not None:
    try:
        budget_limit = float(budget_limit)
    except (TypeError, ValueError):
        budget_limit = None

# Prefer the recorded cumulative spend; fall back to the current run only when
# the state file carries no figure. Never silently substitute 0.0 for "unknown".
# A recorded ZERO is not a reading: run.sh check_budget_limit writes
# "budget_used": 0 for a run it never measured. Same rule as the dashboard
# (server.py budget readers): only a positive recorded figure counts, and a
# measured zero comes from current_cost, which collect_efficiency leaves None
# unless a record passes record_is_measured.
if isinstance(budget_used_recorded, (int, float)) and budget_used_recorded > 0:
    budget_used = budget_used_recorded
elif isinstance(current_cost, (int, float)):
    budget_used = current_cost
else:
    budget_used = None
status = "none"
percent_used = None
remaining = None
if budget_limit is not None and budget_limit > 0 and budget_used is None:
    status = "unknown"
elif budget_limit is not None and budget_limit > 0:
    percent_used = round(budget_used / budget_limit * 100, 2)
    remaining = max(0.0, budget_limit - budget_used)
    if budget_used >= budget_limit:
        status = "exceeded"
    elif budget_used >= 0.80 * budget_limit:
        status = "warn"
    else:
        status = "ok"

# --- model routing by spend (from run history) ---------------------------
by_model = {}
for r in runs:
    c = r.get("cost_usd")
    if c is None:
        continue
    m = r.get("model") or "unknown"
    by_model[m] = by_model.get(m, 0.0) + c

if cost_csv:
    import csv
    ws = os.path.basename(os.path.dirname(os.path.abspath(loki_dir))) or "unknown"
    who = os.environ.get("USER") or os.environ.get("LOGNAME") or "unknown"
    w = csv.writer(sys.stdout, lineterminator="\n")
    w.writerow(["repo", "user", "usd", "verdict", "run_id", "date"])
    for r in sorted(runs, key=lambda x: (x.get("generated_at") or "", str(x.get("run_id")))):
        c = r.get("cost_usd")
        w.writerow([ws, who, "unmeasured" if c is None else "%.6f" % c,
                    r.get("final_verdict") or "unknown", r.get("run_id"),
                    (r.get("generated_at") or "")[:10]])
    sys.exit(0)

if show_json:
    out = {
        "current_run": {
            "cost_usd": current_cost,
            "model": current_model or None,
            "cost_recorded": current_cost is not None,
            "tokens": current_tokens,
            "cost_lib_available": lib_available,
        },
        "runs": runs,
        "runs_count": len(runs),
        "project_total_usd": round(project_total, 6) if runs else 0.0,
        "by_model": {k: round(v, 6) for k, v in by_model.items()},
        "budget": {
            "limit": budget_limit,
            "used": round(budget_used, 6) if budget_used is not None else None,
            "remaining": round(remaining, 6) if remaining is not None else None,
            "percent_used": percent_used,
            "status": status,
            "warn_threshold_percent": 80,
            "exceeded": status == "exceeded",
        },
    }
    print(json.dumps(out, indent=2))
    sys.exit(0)

# --- human-readable ------------------------------------------------------
ds = chr(36)
print()
print(BOLD + "Loki Cost" + NC)
print(DIM + "=" * 50 + NC)

print()
print(CYAN + "Current run" + NC)
if not lib_available:
    print(DIM + "  Cost library unavailable (efficiency_cost.py not found)." + NC)
    print(DIM + "  Current-run spend cannot be computed on this install." + NC)
elif current_cost is None:
    print("  Cost not recorded for this run.")
else:
    mtxt = (" (" + current_model + ")") if current_model else ""
    print("  Spend: " + BOLD + _fmt_usd(current_cost) + NC + mtxt)

print()
print(CYAN + "Project history" + NC)
print("  Runs recorded: " + str(len(runs)))
print("  Total spend:   " + BOLD + (_fmt_usd(project_total) if runs else "$0.00") + NC)

if by_model:
    print()
    print(CYAN + "Model routing (by spend)" + NC)
    total_m = sum(by_model.values()) or 1.0
    for m in sorted(by_model, key=lambda k: by_model[k], reverse=True):
        v = by_model[m]
        pct = v / total_m * 100
        bar_len = int(pct / 5)
        bar = "#" * bar_len + "." * (20 - bar_len)
        print("  {:<16} {}{:>9} ({:4.1f}%) [{}]".format(m[:16], "", _fmt_usd(v), pct, bar))

print()
print(CYAN + "Budget" + NC)
if budget_limit is None:
    print("  No cap set. Set LOKI_BUDGET_LIMIT (USD) to cap spend.")
    print(DIM + "  When set, Loki warns at 80% and hard-stops at 100%." + NC)
else:
    col = GREEN
    if status in ("warn", "unknown"):
        col = YELLOW
    elif status == "exceeded":
        col = RED
    print("  Cap:       " + _fmt_usd(budget_limit))
    if budget_used is None:
        print("  Used:      not recorded (no measured iteration cost)")
        print("  Remaining: not recorded")
    else:
        print("  Used:      " + _fmt_usd(budget_used) + " (" + col + str(percent_used) + "%" + NC + ")")
        print("  Remaining: " + _fmt_usd(remaining))
    print("  Status:    " + col + BOLD + status.upper() + NC)
    if status == "warn":
        print(YELLOW + "  Warning: at or above 80% of cap. Run continues; hard-stop at 100%." + NC)
    elif status == "exceeded":
        print(RED + "  Cap reached. The run is paused to prevent a surprise bill." + NC)

if runs:
    print()
    print(CYAN + "Recent runs" + NC)
    print(DIM + "  {:<28} {:<10} {:>9}  {}".format("Run", "Model", "Cost", "Verdict") + NC)
    for r in runs[:max(last_n, 10) if last_n else 10]:
        rid = str(r.get("run_id") or "")[:28]
        mdl = str(r.get("model") or "")[:10]
        cst = _fmt_usd(r.get("cost_usd"))
        vrd = str(r.get("final_verdict") or "")
        print("  {:<28} {:<10} {:>9}  {}".format(rid, mdl, cst, vrd))

print()
print(DIM + "Dashboard cost panel: /cost   |   JSON: loki cost --json" + NC)
print()
COST_SCRIPT
}

# Fetch and display Prometheus metrics from dashboard
cmd_metrics() {
    local show_json=false
    local last_n=0
    local save_file=false
    local share_flag=false
    local share_visibility=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki metrics${NC} - Session productivity reporter"
                echo ""
                echo "Usage: loki metrics [options]"
                echo ""
                echo "Analyzes past Loki Mode session data and generates a productivity"
                echo "stats report showing agents deployed, iterations completed, files"
                echo "changed, and estimated time saved."
                echo ""
                echo "Options:"
                echo "  --json               Machine-readable JSON output"
                echo "  --last N             Analyze only the last N sessions (default: all)"
                echo "  --save               Write report to METRICS.md in project root"
                echo "  --share              Upload report as a secret GitHub Gist (via loki share)"
                echo "  --public             With --share, create a PUBLIC gist instead"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Subcommands:"
                echo "  prometheus           Fetch Prometheus/OpenMetrics from dashboard"
                echo ""
                echo "Examples:"
                echo "  loki metrics                   # Full productivity report"
                echo "  loki metrics --json            # Machine-readable output"
                echo "  loki metrics --last 5          # Last 5 sessions only"
                echo "  loki metrics --save            # Write METRICS.md"
                echo "  loki metrics --share           # Share as a secret GitHub Gist"
                echo "  loki metrics prometheus        # Prometheus metrics from dashboard"
                exit 0
                ;;
            --json) show_json=true; shift ;;
            --last) last_n="${2:-0}"; shift 2 ;;
            --last=*) last_n="${1#*=}"; shift ;;
            --save) save_file=true; shift ;;
            --share) share_flag=true; shift ;;
            --public) share_visibility="--public"; shift ;;
            prometheus)
                # Legacy Prometheus metrics subcommand
                shift
                local port="${LOKI_DASHBOARD_PORT:-57374}"
                local host="127.0.0.1"
                local url="http://${host}:${port}/metrics"
                local response
                response=$(curl -sf "$url" 2>/dev/null) || true
                if [ -z "$response" ]; then
                    echo -e "${RED}Error: Could not connect to dashboard at ${url}${NC}"
                    echo "Make sure the dashboard is running: loki serve"
                    exit 1
                fi
                echo "$response"
                exit 0
                ;;
            *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki metrics --help' for usage." >&2; exit 1 ;;
        esac
    done

    local loki_dir="${LOKI_DIR:-.loki}"

    if ! command -v python3 &>/dev/null; then
        echo -e "${RED}python3 is required for metrics generation${NC}"
        exit 1
    fi

    local metrics_output
    metrics_output=$(LOKI_DIR="$loki_dir" \
        LOKI_SKILL_DIR="$SKILL_DIR" \
        METRICS_JSON="$show_json" \
        METRICS_LAST_N="$last_n" \
        METRICS_SAVE="$save_file" \
        python3 << 'METRICS_SCRIPT'
import json
import os
import sys
import glob
import subprocess
from datetime import datetime

loki_dir = os.environ.get("LOKI_DIR", ".loki")
show_json = os.environ.get("METRICS_JSON", "false") == "true"
last_n = int(os.environ.get("METRICS_LAST_N", "0"))
save_flag = os.environ.get("METRICS_SAVE", "false") == "true"

def load_json(path):
    try:
        with open(path) as f:
            return json.load(f)
    except Exception:
        return None

def fmt_number(n):
    if n is None:
        return "N/A"
    return f"{n:,}"

def load_queue(path):
    data = load_json(path)
    if isinstance(data, list):
        return data
    if isinstance(data, dict) and "tasks" in data:
        return data["tasks"]
    return []

# --- Gather project name ---
project_name = os.path.basename(os.getcwd())

# --- Session data ---
sessions_analyzed = 0
total_iterations = 0
total_tasks_completed = 0
total_tasks_failed = 0
agent_types_seen = set()
total_duration_seconds = 0

# Count sessions from checkpoints index or autonomy-state
checkpoint_index = os.path.join(loki_dir, "state", "checkpoints", "index.jsonl")
session_entries = []
if os.path.isfile(checkpoint_index):
    try:
        with open(checkpoint_index) as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        session_entries.append(json.loads(line))
                    except Exception:
                        pass
    except Exception:
        pass

# Orchestrator state
orch_raw = load_json(os.path.join(loki_dir, "state", "orchestrator.json"))
orch = orch_raw or {}
current_phase = orch.get("currentPhase", "N/A")
orch_metrics = orch.get("metrics", {})
tasks_completed_orch = orch_metrics.get("tasksCompleted", 0)
tasks_failed_orch = orch_metrics.get("tasksFailed", 0)

# Per-iteration efficiency files
eff_dir = os.path.join(loki_dir, "metrics", "efficiency")
iterations = []
if os.path.isdir(eff_dir):
    for path in sorted(glob.glob(os.path.join(eff_dir, "iteration-*.json"))):
        data = load_json(path)
        if data:
            iterations.append(data)

# Apply --last N filter
if last_n > 0 and len(iterations) > last_n:
    iterations = iterations[-last_n:]

# BACKLOG 118 / S-147: a fresh .loki with no efficiency files and no
# orchestrator state has nothing to count. total_iterations (and the
# time_saved estimate derived from it) must come back null, not a
# fabricated 0, so callers can tell "never ran" from "ran zero".
if iterations:
    total_iterations = len(iterations)
    iterations_known = True
elif orch_raw is not None and "currentIteration" in orch:
    total_iterations = max(orch.get("currentIteration", 0), 0)
    iterations_known = True
else:
    total_iterations = None
    iterations_known = False

# Gather agent types from iterations and agents.json
for it in iterations:
    model = it.get("model", "")
    if model:
        agent_types_seen.add(model)
    total_duration_seconds += it.get("duration_seconds", 0)

agents_file = os.path.join(loki_dir, "state", "agents.json")
agents_data = load_json(agents_file)
if isinstance(agents_data, list):
    for agent in agents_data:
        atype = agent.get("agent_type", "")
        if atype:
            agent_types_seen.add(atype)

# Queue data for task counts
completed_tasks = load_queue(os.path.join(loki_dir, "queue", "completed.json"))
failed_tasks = load_queue(os.path.join(loki_dir, "queue", "failed.json"))
pending_tasks = load_queue(os.path.join(loki_dir, "queue", "pending.json"))
in_progress_tasks = load_queue(os.path.join(loki_dir, "queue", "in-progress.json"))

total_tasks_completed = len(completed_tasks) if completed_tasks else tasks_completed_orch
total_tasks_failed = len(failed_tasks) if failed_tasks else tasks_failed_orch
total_tasks = total_tasks_completed + total_tasks_failed + len(pending_tasks) + len(in_progress_tasks)
success_rate = (total_tasks_completed / total_tasks * 100) if total_tasks > 0 else 0.0

# Session count: count unique session directories or fall back to 1 if .loki exists
sessions_dir = os.path.join(loki_dir, "state", "sessions")
if os.path.isdir(sessions_dir):
    sessions_analyzed = len([d for d in os.listdir(sessions_dir) if os.path.isdir(os.path.join(sessions_dir, d))])
if sessions_analyzed == 0:
    # Check autonomy-state.json retryCount as proxy for session attempts
    auto_state = load_json(os.path.join(loki_dir, "autonomy-state.json"))
    if auto_state:
        sessions_analyzed = max(1, auto_state.get("retryCount", 1))
    elif os.path.isdir(loki_dir):
        sessions_analyzed = 1

# Apply --last N to sessions
if last_n > 0:
    sessions_analyzed = min(sessions_analyzed, last_n)

# --- Git stats ---
lines_added = 0
lines_removed = 0
commits_made = 0
files_changed_git = 0
tests_written = 0

try:
    result = subprocess.run(
        ["git", "log", "--shortstat", "--format="],
        capture_output=True, text=True, timeout=10
    )
    if result.returncode == 0:
        for line in result.stdout.strip().split("\n"):
            line = line.strip()
            if not line:
                continue
            parts = line.split(",")
            for part in parts:
                part = part.strip()
                if "file" in part and "changed" in part:
                    try:
                        files_changed_git += int(part.split()[0])
                    except (ValueError, IndexError):
                        pass
                elif "insertion" in part:
                    try:
                        lines_added += int(part.split()[0])
                    except (ValueError, IndexError):
                        pass
                elif "deletion" in part:
                    try:
                        lines_removed += int(part.split()[0])
                    except (ValueError, IndexError):
                        pass

    # Count commits
    result2 = subprocess.run(
        ["git", "rev-list", "--count", "HEAD"],
        capture_output=True, text=True, timeout=10
    )
    if result2.returncode == 0:
        commits_made = int(result2.stdout.strip())
except Exception:
    pass

# Estimate tests written from git log (files matching test/spec patterns)
try:
    result3 = subprocess.run(
        ["git", "log", "--diff-filter=A", "--name-only", "--format="],
        capture_output=True, text=True, timeout=10
    )
    if result3.returncode == 0:
        for fname in result3.stdout.strip().split("\n"):
            fname = fname.strip().lower()
            if any(kw in fname for kw in ["test", "spec", ".test.", ".spec.", "_test.", "_spec."]):
                tests_written += 1
except Exception:
    pass

# --- Token/cost data ---
total_cost = sum(it.get("cost_usd", 0) for it in iterations)
total_input_tokens = sum(it.get("input_tokens", 0) for it in iterations)
total_output_tokens = sum(it.get("output_tokens", 0) for it in iterations)
tokens_known = bool(iterations)

# Also check context tracking for cost data
ctx_file = os.path.join(loki_dir, "context", "tracking.json")
ctx = load_json(ctx_file)
# context-tracker.py seeds tracking.json with all-zero totals, so the file
# existing is not a measurement: only real (nonzero) totals count.
if isinstance(ctx, dict) and not tokens_known:
    totals = ctx.get("totals")
    if isinstance(totals, dict) and (totals.get("total_input") or totals.get("total_output")
                                     or totals.get("total_cost_usd") or totals.get("iterations_tracked")):
        total_cost = totals.get("total_cost_usd") or 0
        total_input_tokens = totals.get("total_input") or 0
        total_output_tokens = totals.get("total_output") or 0
        tokens_known = True

# --- Time saved estimate ---
# Each Loki iteration replaces ~15min of manual work. Null (not a computed
# number) when no iteration was ever recorded (BACKLOG 118 / S-147).
time_saved_hours = round(total_iterations * 15 / 60, 2) if iterations_known else None

# --- Memory stats ---
episodic_count = 0
semantic_count = 0
episodic_dir = os.path.join(loki_dir, "memory", "episodic")
semantic_dir = os.path.join(loki_dir, "memory", "semantic")
if os.path.isdir(episodic_dir):
    for root, dirs, files in os.walk(episodic_dir):
        episodic_count += len([f for f in files if f.endswith(".json")])
if os.path.isdir(semantic_dir):
    for root, dirs, files in os.walk(semantic_dir):
        semantic_count += len([f for f in files if f.endswith(".json")])

# --- Build output ---
version = "6.33.0"
try:
    skill_dir = os.environ.get("LOKI_SKILL_DIR", "")
    version_candidates = [
        os.path.join(skill_dir, "VERSION") if skill_dir else "",
        "VERSION",
        "../VERSION",
        os.path.join(loki_dir, "..", "VERSION"),
    ]
    for vf in version_candidates:
        if vf and os.path.isfile(vf):
            with open(vf) as f:
                version = f.read().strip()
            break
except Exception:
    pass

data = {
    "project": project_name,
    "sessions_analyzed": sessions_analyzed,
    "agent_activity": {
        "total_iterations": total_iterations,
        "agent_types": len(agent_types_seen),
        "tasks_completed": total_tasks_completed,
        "tasks_failed": total_tasks_failed,
        "success_rate": round(success_rate, 1)
    },
    "code_output": {
        "files_changed": files_changed_git,
        "lines_added": lines_added,
        "lines_removed": lines_removed,
        "commits": commits_made,
        "tests_written": tests_written
    },
    "tokens": {
        "input": total_input_tokens if tokens_known else None,
        "output": total_output_tokens if tokens_known else None,
        "total": (total_input_tokens + total_output_tokens) if tokens_known else None,
        "cost_usd": round(total_cost, 2) if tokens_known else None
    },
    "time_saved": {
        "hours": time_saved_hours,
        "iterations": total_iterations,
        "minutes_per_iteration": 15
    },
    "memory": {
        "episodic_entries": episodic_count,
        "semantic_patterns": semantic_count
    },
    "version": version
}

if show_json:
    print(json.dumps(data, indent=2))
else:
    # ASCII stats card
    lines = []
    lines.append("")
    lines.append("LOKI MODE PRODUCTIVITY REPORT")
    lines.append("=" * 40)
    lines.append(f"Project: {project_name}")
    lines.append(f"Sessions analyzed: {sessions_analyzed}")
    lines.append("")
    lines.append("AGENT ACTIVITY")
    lines.append(f"  Total iterations:    {fmt_number(total_iterations)}")
    lines.append(f"  Agent types used:    {len(agent_types_seen)}")
    lines.append(f"  Tasks completed:     {fmt_number(total_tasks_completed)}")
    if total_tasks_failed > 0:
        lines.append(f"  Tasks failed:        {fmt_number(total_tasks_failed)}")
    if total_tasks > 0:
        lines.append(f"  Success rate:        {success_rate:.1f}%")
    lines.append("")
    lines.append("CODE OUTPUT")
    lines.append(f"  Files changed:       {fmt_number(files_changed_git)}")
    lines.append(f"  Lines added:         {fmt_number(lines_added)}")
    lines.append(f"  Lines removed:       {fmt_number(lines_removed)}")
    lines.append(f"  Commits:             {fmt_number(commits_made)}")
    lines.append(f"  Tests written:       {fmt_number(tests_written)}")
    lines.append("")
    if tokens_known and (total_cost > 0 or total_input_tokens > 0):
        lines.append("TOKEN USAGE")
        lines.append(f"  Input tokens:        {fmt_number(total_input_tokens)}")
        lines.append(f"  Output tokens:       {fmt_number(total_output_tokens)}")
        lines.append(f"  Estimated cost:      ${total_cost:.2f}")
        lines.append("")
    lines.append("TIME SAVED")
    if iterations_known:
        lines.append(f"  Estimated:           {time_saved_hours} hours")
        lines.append(f"  (based on {fmt_number(total_iterations)} iterations x 15min each)")
    else:
        lines.append("  Estimated:           unknown (no iterations recorded)")
    lines.append("")
    if episodic_count > 0 or semantic_count > 0:
        lines.append("MEMORY")
        lines.append(f"  Episodic entries:    {fmt_number(episodic_count)}")
        lines.append(f"  Semantic patterns:   {fmt_number(semantic_count)}")
        lines.append("")
    lines.append(f"Generated by Loki Mode v{version} | autonomi.dev")
    lines.append("Share your stats: loki metrics --share")
    lines.append("")

    output_text = "\n".join(lines)
    print(output_text)

    # Save to METRICS.md if requested
    if save_flag:
        try:
            with open("METRICS.md", "w") as f:
                f.write("# Loki Mode Productivity Report\n\n")
                f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
                f.write("```\n")
                f.write(output_text.strip())
                f.write("\n```\n")
            print("Saved to METRICS.md")
        except Exception as e:
            print(f"Error saving METRICS.md: {e}", file=sys.stderr)

METRICS_SCRIPT
    )
    local metrics_exit=$?

    if [ $metrics_exit -ne 0 ]; then
        echo -e "${RED}Failed to generate metrics report${NC}"
        echo "$metrics_output"
        exit 1
    fi

    echo "$metrics_output"

    # Handle --share flag
    if [ "$share_flag" = true ]; then
        local tmpdir tmpfile
        tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/loki-metrics.XXXXXXXX") || exit 1
        tmpfile="$tmpdir/metrics.md"
        echo "$metrics_output" > "$tmpfile"

        if ! command -v gh &>/dev/null; then
            echo -e "${RED}gh CLI not found -- cannot share${NC}"
            echo "Install: brew install gh"
            rm -f "$tmpfile"; rmdir "$tmpdir" 2>/dev/null || true
            exit 1
        fi

        if ! gh auth status &>/dev/null 2>&1; then
            echo -e "${RED}GitHub CLI not authenticated${NC}"
            echo "Run 'gh auth login' to authenticate."
            rm -f "$tmpfile"; rmdir "$tmpdir" 2>/dev/null || true
            exit 1
        fi

        echo "Uploading metrics report..."
        local project_name
        project_name=$(basename "$(pwd)")
        local gist_desc="Loki Mode productivity report - ${project_name:-project} ($(date +%Y-%m-%d))"
        local gist_url
        if [ "$share_visibility" = "--public" ]; then
            echo -e "${YELLOW}Warning: this gist is PUBLIC and visible to anyone with the URL.${NC}" >&2
        fi
        gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" $share_visibility 2>&1)
        local gist_exit=$?
        rm -f "$tmpfile"; rmdir "$tmpdir" 2>/dev/null || true

        if [ $gist_exit -ne 0 ]; then
            echo -e "${RED}Failed to create gist${NC}"
            echo "$gist_url"
            exit 1
        fi
        echo -e "${GREEN}Shared: ${gist_url}${NC}"
    fi
}

# CLI consolidation (Phase B, design item 8): the 'analyze' noun groups the
# repo-intelligence surface (explain/onboard/code/context) under one entry. It
# is a THIN dispatcher: every arm forwards verbatim to the existing cmd_*
# handler (no handler logic moves). The four top-level tokens remain deprecated
# forwarding aliases for back-compat.
cmd_analyze() {
    local subcommand="${1:-help}"
    case "$subcommand" in
        explain)  shift; cmd_explain "$@"; return $? ;;
        onboard)  shift; cmd_onboard "$@"; return $? ;;
        code)     shift; cmd_code "$@";    return $? ;;
        context)  shift; cmd_context "$@"; return $? ;;
        --help|-h|help)
            echo -e "${BOLD}loki analyze${NC} - Project knowledge and codebase intelligence"
            echo ""
            echo "Usage: loki analyze <subcommand> [args]"
            echo ""
            echo "Subcommands (grouped knowledge surface):"
            echo "  explain [path]   Explain a codebase architecture in prose (was: loki explain)"
            echo "  onboard [path]   Analyze a repo and generate CLAUDE.md (was: loki onboard)"
            echo "  code [query]     Codebase intelligence queries (was: loki code)"
            echo "  context [cmd]    Context-window management (was: loki context)"
            echo ""
            echo "Run 'loki analyze <subcommand> --help' for each. The old top-level"
            echo "names still work; see 'loki help aliases'."
            return 0
            ;;
        *)
            echo -e "${RED}Unknown analyze command: $subcommand${NC}" >&2
            echo "Run 'loki analyze --help' for usage." >&2
            return 1
            ;;
    esac
}

# Context window management (inspired by Kiro CLI /context command)
# Shows token usage breakdown and context window utilization
cmd_context() {
    local subcommand="${1:-show}"
    shift 2>/dev/null || true

    case "$subcommand" in
        --help|-h|help)
            echo -e "${BOLD}loki context${NC} - Context window management"
            echo ""
            echo "Usage: loki context <subcommand>"
            echo ""
            echo "Subcommands:"
            echo "  show       Show context window usage breakdown (default)"
            echo "  files      List files currently in context with token estimates"
            echo "  tools      Show tool token usage per origin (MCP, native)"
            echo "  add        Add file to context (@file reference expansion)"
            echo "  clear      Clear accumulated context (start fresh conversation)"
            echo ""
            echo "Examples:"
            echo "  loki context                    # Show context usage"
            echo "  loki context files              # List context files"
            echo "  loki context tools              # Show tool token costs"
            echo "  loki context add src/main.py    # Add file to context"
            echo ""
            echo "Inspired by: Kiro CLI /context command (kiro.dev)"
            return 0
            ;;
        show)
            _context_show "$@"
            ;;
        files)
            _context_files "$@"
            ;;
        tools)
            _context_tools "$@"
            ;;
        add)
            _context_add "$@"
            ;;
        clear)
            _context_clear "$@"
            ;;
        *)
            echo -e "${RED}Unknown subcommand: $subcommand${NC}"
            echo "Run 'loki context help' for usage."
            return 1
            ;;
    esac
}

_context_show() {
    local loki_dir="${LOKI_DIR:-.loki}"

    if [ ! -d "$loki_dir" ]; then
        echo -e "${YELLOW}No active session.${NC}"
        return 0
    fi

    section_header "Context Window Usage" 2>/dev/null || echo -e "\n${BOLD}Context Window Usage${NC}"

    # Read context tracker data if available
    local ctx_file="$loki_dir/state/context-usage.json"
    if [ -f "$ctx_file" ]; then
        local total_tokens used_tokens input_tokens output_tokens cache_tokens
        total_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
        used_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
        input_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('input_tokens', 0))" 2>/dev/null || echo "0")
        output_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('output_tokens', 0))" 2>/dev/null || echo "0")
        cache_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('cache_read_tokens', 0))" 2>/dev/null || echo "0")

        # Display gauge
        if type context_gauge &>/dev/null; then
            context_gauge "$used_tokens" "$total_tokens" "Window"
        else
            local pct=0
            if [ "$total_tokens" -gt 0 ] 2>/dev/null; then
                pct=$((used_tokens * 100 / total_tokens))
            fi
            echo -e "  ${CYAN}Context:${NC} ${pct}% used (${used_tokens} / ${total_tokens} tokens)"
        fi
        echo ""

        # Breakdown
        echo -e "  ${BOLD}Breakdown${NC} ${DIM}(estimated)${NC}"
        echo -e "  ${DIM}Input tokens:${NC}  $(printf '%8s' "$input_tokens")"
        echo -e "  ${DIM}Output tokens:${NC} $(printf '%8s' "$output_tokens")"
        echo -e "  ${DIM}Cache reads:${NC}   $(printf '%8s' "$cache_tokens")"
    else
        echo -e "  ${DIM}No context tracking data yet.${NC}"
        echo -e "  ${DIM}Context usage is tracked during active sessions.${NC}"
    fi

    # Show token costs if budget file exists
    local budget_file="$loki_dir/metrics/budget.json"
    if [ -f "$budget_file" ]; then
        echo ""
        echo -e "  ${BOLD}Cost${NC}"
        local budget_used budget_limit
        budget_used=$(_BUDGET_FILE="$budget_file" python3 -c "import json, os; d=json.load(open(os.environ['_BUDGET_FILE'])); print(round(d.get('budget_used', 0), 4))" 2>/dev/null || echo "0")
        budget_limit=$(_BUDGET_FILE="$budget_file" python3 -c "import json, os; d=json.load(open(os.environ['_BUDGET_FILE'])); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")

        if [ "$budget_limit" != "0" ]; then
            echo -e "  ${DIM}Spent:${NC}     \$${budget_used} / \$${budget_limit}"
            local remaining
            remaining=$(echo "scale=4; $budget_limit - $budget_used" | bc 2>/dev/null || echo "0")
            echo -e "  ${DIM}Remaining:${NC} \$${remaining}"
        else
            echo -e "  ${DIM}Spent:${NC} \$${budget_used} (no budget limit set)"
        fi
    fi
    echo ""
}

_context_files() {
    local loki_dir="${LOKI_DIR:-.loki}"
    local ctx_files="$loki_dir/state/context-files.json"

    section_header "Context Files" 2>/dev/null || echo -e "\n${BOLD}Context Files${NC}"

    if [ -f "$ctx_files" ]; then
        python3 << 'PYEOF'
import json, os

ctx_file = os.environ.get("LOKI_DIR", ".loki") + "/state/context-files.json"
try:
    with open(ctx_file) as f:
        files = json.load(f)
    total = 0
    for entry in files:
        name = entry.get("path", "unknown")
        tokens = entry.get("estimated_tokens", 0)
        total += tokens
        print(f"  {tokens:>8} tokens  {name}")
    print(f"\n  {'Total:':>8}  {total} tokens")
except Exception:
    print("  No context files tracked yet.")
PYEOF
    else
        echo -e "  ${DIM}No context files tracked.${NC}"
        echo ""
        echo "  Add files with: loki context add <file>"
        echo "  Or use @path syntax in your prompt:"
        echo "    @src/main.py    - inject file contents"
        echo "    @src/           - inject directory tree"
    fi
    echo ""
}

_context_tools() {
    local loki_dir="${LOKI_DIR:-.loki}"

    section_header "Tool Token Usage" 2>/dev/null || echo -e "\n${BOLD}Tool Token Usage${NC}"

    # Read MCP config and estimate tool token usage
    local mcp_config="$loki_dir/mcp/config.json"
    if [ -f "$mcp_config" ]; then
        python3 << 'PYEOF'
import json, os

config_file = os.environ.get("LOKI_DIR", ".loki") + "/mcp/config.json"
try:
    with open(config_file) as f:
        config = json.load(f)
    servers = config.get("mcpServers", {})
    total = 0
    for name, server in servers.items():
        tools = server.get("tools", [])
        # Estimate ~500 tokens per tool definition
        est = len(tools) * 500 if tools else 500
        total += est
        print(f"  {est:>6} tokens  {name} ({len(tools) if tools else '?'} tools)")
    print(f"\n  Total MCP tool overhead: ~{total} tokens per request")
except Exception:
    print("  No MCP servers configured.")
PYEOF
    else
        echo -e "  ${DIM}Native tools only (no MCP servers configured).${NC}"
    fi

    echo ""
    echo -e "  ${DIM}Native tools: ~2000 tokens (bash, read, write, glob, grep)${NC}"
    echo -e "  ${DIM}Tip: Large MCP tool descriptions impact performance.${NC}"
    echo ""
}

_context_add() {
    local file_path="$1"
    if [ -z "$file_path" ]; then
        echo -e "${RED}Usage: loki context add <file-or-dir>${NC}"
        echo ""
        echo "Adds a file's content (or directory tree) to the context."
        echo "Equivalent to @path inline reference syntax."
        return 1
    fi

    # Strip @ prefix if user types it
    file_path="${file_path#@}"

    if [ -d "$file_path" ]; then
        echo -e "${BOLD}Directory tree: ${file_path}${NC}"
        echo ""
        if type tree_display &>/dev/null; then
            tree_display "$file_path"
        else
            find "$file_path" -maxdepth 3 -not -path '*/\.*' | head -50
        fi
        echo ""

        # Count files and estimate tokens
        local file_count
        file_count=$(find "$file_path" -type f -not -path '*/\.*' | wc -l)
        echo -e "${DIM}$file_count files in directory${NC}"
    elif [ -f "$file_path" ]; then
        local size
        size=$(wc -c < "$file_path")
        # Rough estimate: 1 token per 4 bytes
        local est_tokens=$((size / 4))
        local lines
        lines=$(wc -l < "$file_path")

        echo -e "${BOLD}File: ${file_path}${NC}"
        echo -e "  ${DIM}Size:${NC}    $size bytes"
        echo -e "  ${DIM}Lines:${NC}   $lines"
        echo -e "  ${DIM}Tokens:${NC}  ~$est_tokens (estimated)"

        # Store in context files list
        local loki_dir="${LOKI_DIR:-.loki}"
        mkdir -p "$loki_dir/state"
        local ctx_files="$loki_dir/state/context-files.json"
        # Pass values via env vars (not string interpolation) so a filename
        # with an apostrophe or other special char cannot break the python
        # source (was a swallowed SyntaxError -> set -e abort -> file silently
        # not added). Matches the safe pattern used by the registry commands.
        _CTX_PATH="$file_path" _CTX_TOKENS="$est_tokens" _CTX_SIZE="$size" \
        _CTX_LINES="$lines" _CTX_FILE="$ctx_files" python3 -c "
import json, os
path = os.environ['_CTX_PATH']
tokens = int(os.environ['_CTX_TOKENS'])
ctx_file = os.environ['_CTX_FILE']
size = int(os.environ['_CTX_SIZE'])
lines = int(os.environ['_CTX_LINES'])
try:
    with open(ctx_file) as f:
        files = json.load(f)
except:
    files = []
# Avoid duplicates
files = [f for f in files if f.get('path') != path]
files.append({'path': path, 'estimated_tokens': tokens, 'size': size, 'lines': lines})
with open(ctx_file, 'w') as f:
    json.dump(files, f, indent=2)
" 2>/dev/null

        echo -e "  ${GREEN}Added to context.${NC}"
    else
        echo -e "${RED}Not found: $file_path${NC}"
        return 1
    fi
}

_context_clear() {
    local loki_dir="${LOKI_DIR:-.loki}"
    rm -f "$loki_dir/state/context-files.json"
    rm -f "$loki_dir/state/context-usage.json"
    echo -e "${GREEN}Context cleared.${NC}"
    echo -e "${DIM}A new conversation will start fresh.${NC}"
}

# Codebase intelligence (inspired by Kiro CLI /code command)
# Provides workspace overview, symbol search, and pattern matching
cmd_code() {
    local subcommand="${1:-help}"
    shift 2>/dev/null || true

    case "$subcommand" in
        --help|-h|help)
            echo -e "${BOLD}loki code${NC} - Codebase intelligence"
            echo ""
            echo "Usage: loki code <subcommand>"
            echo ""
            echo "Subcommands:"
            echo "  overview    Generate workspace overview (file types, LOC, structure)"
            echo "  symbols     Search for symbols (functions, classes, variables)"
            echo "  search      Hybrid grep + semantic codebase search"
            echo "  deps        Show dependency graph for a file or module"
            echo "  hotspots    Find code hotspots (most-changed files, complexity)"
            echo "  diff        Show colored diff of recent changes"
            echo ""
            echo "Examples:"
            echo "  loki code overview                  # Full codebase overview"
            echo "  loki code overview --silent          # Compact output"
            echo "  loki code symbols 'class.*Service'   # Find service classes"
            echo "  loki code search 'rate limit backoff'   # Hybrid grep + semantic search"
            echo "  loki code search 'council vote' --grep-only   # Lexical only"
            echo "  loki code search 'memory retrieval' --semantic-only --top 15"
            echo "  loki code deps src/main.py           # Show dependencies"
            echo "  loki code hotspots --top 10          # Top 10 changed files"
            echo "  loki code diff                       # Colored diff of uncommitted changes"
            echo ""
            echo "Inspired by: Kiro CLI /code command (kiro.dev)"
            return 0
            ;;
        overview)
            _code_overview "$@"
            ;;
        symbols)
            _code_symbols "$@"
            ;;
        search)
            _code_search "$@"
            ;;
        deps)
            _code_deps "$@"
            ;;
        hotspots)
            _code_hotspots "$@"
            ;;
        diff)
            _code_diff "$@"
            ;;
        *)
            echo -e "${RED}Unknown subcommand: $subcommand${NC}"
            echo "Run 'loki code help' for usage."
            return 1
            ;;
    esac
}

# Hybrid codebase search: grep + semantic (ChromaDB) fused with RRF.
# Delegates to tools/hybrid_search.py (needs python3.12 for chromadb). The
# Python side handles the rg / grep / python-scan fallback and the grep-only
# degradation when ChromaDB is unreachable, so this stays thin.
_code_search() {
    local query=""
    local -a pyargs=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --grep-only) pyargs+=("--grep-only"); shift ;;
            --semantic-only) pyargs+=("--semantic-only"); shift ;;
            --budget) pyargs+=("--budget" "${2:-3000}"); shift 2 ;;
            --top) pyargs+=("--top" "${2:-10}"); shift 2 ;;
            --json) pyargs+=("--json"); shift ;;
            -h|--help)
                echo -e "${BOLD}loki code search${NC} - Hybrid grep + semantic codebase search"
                echo ""
                echo "Usage: loki code search \"<query>\" [flags]"
                echo ""
                echo "Flags:"
                echo "  --grep-only        Lexical search only (skip semantic)"
                echo "  --semantic-only    Semantic search only (skip grep)"
                echo "  --budget N         Token budget for merged results (default 3000)"
                echo "  --top N            Max results (default 10)"
                echo "  --json             Output JSON"
                echo ""
                echo "Falls back to grep-only when ChromaDB is unreachable."
                return 0
                ;;
            *)
                if [[ -z "$query" ]]; then
                    query="$1"
                else
                    query="$query $1"
                fi
                shift
                ;;
        esac
    done

    if [[ -z "$query" ]]; then
        echo -e "${RED}Usage: loki code search \"<query>\" [--grep-only|--semantic-only] [--budget N] [--top N]${NC}"
        return 1
    fi

    local script_dir hybrid py
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
    hybrid="$script_dir/tools/hybrid_search.py"
    if [[ ! -f "$hybrid" ]]; then
        echo -e "${RED}hybrid_search.py not found at $hybrid${NC}"
        return 1
    fi

    # chromadb requires python3.12 on this stack; fall back to python3 for the
    # grep-only path if 3.12 is absent.
    if [[ -x /opt/homebrew/bin/python3.12 ]]; then
        py=/opt/homebrew/bin/python3.12
    else
        py="$(command -v python3 || true)"
    fi
    if [[ -z "$py" ]]; then
        echo -e "${RED}python3 not found${NC}"
        return 1
    fi

    # Safe array expansion: "${pyargs[@]}" triggers "unbound variable" under
    # bash 3.2 (macOS default) + set -u when the array is empty (no flags).
    "$py" "$hybrid" "$query" ${pyargs[@]+"${pyargs[@]}"}
}

_code_overview() {
    local silent=false
    local target_dir="."
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --silent|-s) silent=true; shift ;;
            *) target_dir="$1"; shift ;;
        esac
    done

    if $silent; then
        echo -e "${BOLD}$(basename "$(cd "$target_dir" && pwd)")${NC}"
    else
        section_header "Codebase Overview: $(basename "$(cd "$target_dir" && pwd)")" 2>/dev/null || echo -e "\n${BOLD}Codebase Overview${NC}"
    fi

    # Language breakdown
    python3 << PYEOF
import os, collections, sys

target = "$target_dir"
silent = $( $silent && echo "True" || echo "False" )

# File extension to language mapping
EXT_MAP = {
    '.py': 'Python', '.js': 'JavaScript', '.ts': 'TypeScript', '.tsx': 'TypeScript',
    '.jsx': 'JavaScript', '.go': 'Go', '.rs': 'Rust', '.java': 'Java',
    '.rb': 'Ruby', '.php': 'PHP', '.c': 'C', '.cpp': 'C++', '.h': 'C/C++ Header',
    '.cs': 'C#', '.swift': 'Swift', '.kt': 'Kotlin', '.scala': 'Scala',
    '.sh': 'Shell', '.bash': 'Shell', '.zsh': 'Shell',
    '.html': 'HTML', '.css': 'CSS', '.scss': 'SCSS', '.vue': 'Vue',
    '.md': 'Markdown', '.json': 'JSON', '.yaml': 'YAML', '.yml': 'YAML',
    '.toml': 'TOML', '.xml': 'XML', '.sql': 'SQL',
    '.dockerfile': 'Docker', '.tf': 'Terraform', '.proto': 'Protobuf',
}

SKIP_DIRS = {'.git', 'node_modules', '.loki', '__pycache__', '.venv', 'venv',
             'dist', 'build', '.next', 'target', '.tox', '.mypy_cache',
             'vendor', '.cargo', 'coverage', '.pytest_cache'}

lang_files = collections.Counter()
lang_lines = collections.Counter()
total_files = 0
total_lines = 0
dir_count = 0

for root, dirs, files in os.walk(target):
    # Skip hidden and build directories
    dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
    dir_count += 1
    for f in files:
        ext = os.path.splitext(f)[1].lower()
        if ext in EXT_MAP:
            lang = EXT_MAP[ext]
            lang_files[lang] += 1
            total_files += 1
            try:
                fp = os.path.join(root, f)
                lines = sum(1 for _ in open(fp, 'rb'))
                lang_lines[lang] += lines
                total_lines += lines
            except:
                pass

if not silent:
    print(f"\n  Files: {total_files}  Lines: {total_lines:,}  Dirs: {dir_count}")
    print()

# Language table
print("  Language         Files   Lines     %")
print("  " + "-" * 40)
for lang, count in lang_lines.most_common(15):
    pct = (count / total_lines * 100) if total_lines > 0 else 0
    files = lang_files[lang]
    bar_len = int(pct / 5)
    bar = "=" * bar_len
    print(f"  {lang:<16} {files:>5}  {count:>7,}  {pct:>4.1f}% {bar}")

if not silent:
    # Show directory structure (top-level)
    print()
    print("  Top-level structure:")
    entries = sorted(os.listdir(target))
    for entry in entries:
        if entry.startswith('.') and entry not in ('.github',):
            continue
        full = os.path.join(target, entry)
        if os.path.isdir(full) and entry not in SKIP_DIRS:
            subcount = sum(1 for _, _, fs in os.walk(full) for f in fs)
            print(f"    {entry + '/':30s} ({subcount} files)")
        elif os.path.isfile(full):
            size = os.path.getsize(full)
            if size > 1024:
                print(f"    {entry:30s} ({size // 1024}KB)")
PYEOF
    echo ""
}

_code_symbols() {
    local pattern="${1:-.}"
    local file_type="${2:-}"

    section_header "Symbol Search: $pattern" 2>/dev/null || echo -e "\n${BOLD}Symbol Search${NC}"

    # Search for function/class definitions using ripgrep or grep
    local search_patterns=(
        "^(def|function|func|fn|pub fn|async def|class|interface|struct|enum|type|export (const|function|class|interface))\s+.*${pattern}"
    )

    if command -v rg &>/dev/null; then
        rg -n --no-heading -e "^(def |function |func |fn |pub fn |async def |class |interface |struct |enum |type |export (const|function|class|interface) ).*${pattern}" \
            --type-add 'code:*.{py,js,ts,tsx,go,rs,java,rb,php,c,cpp,h,cs,swift,kt}' \
            -t code \
            --glob '!node_modules' --glob '!.git' --glob '!dist' --glob '!build' \
            . 2>/dev/null | head -50
    else
        grep -rn "^\(def\|function\|func\|fn\|class\|interface\|struct\|enum\|type\).*${pattern}" \
            --include='*.py' --include='*.js' --include='*.ts' --include='*.go' \
            --include='*.rs' --include='*.java' --include='*.rb' \
            --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist \
            . 2>/dev/null | head -50
    fi
    echo ""
}

_code_deps() {
    local target_file="$1"
    if [ -z "$target_file" ]; then
        echo -e "${RED}Usage: loki code deps <file>${NC}"
        return 1
    fi

    section_header "Dependencies: $target_file" 2>/dev/null || echo -e "\n${BOLD}Dependencies${NC}"

    if [ ! -f "$target_file" ]; then
        echo -e "${RED}File not found: $target_file${NC}"
        return 1
    fi

    local ext="${target_file##*.}"
    echo -e "  ${BOLD}Imports/Requires:${NC}"

    case "$ext" in
        py)
            grep -n "^import\|^from.*import" "$target_file" 2>/dev/null | sed 's/^/  /'
            ;;
        js|ts|tsx|jsx)
            grep -n "import\|require(" "$target_file" 2>/dev/null | sed 's/^/  /'
            ;;
        go)
            grep -n "\"" "$target_file" 2>/dev/null | grep -v "//" | sed 's/^/  /'
            ;;
        rs)
            grep -n "^use\|^extern crate" "$target_file" 2>/dev/null | sed 's/^/  /'
            ;;
        java)
            grep -n "^import" "$target_file" 2>/dev/null | sed 's/^/  /'
            ;;
        rb)
            grep -n "^require\|^require_relative" "$target_file" 2>/dev/null | sed 's/^/  /'
            ;;
        *)
            echo -e "  ${DIM}Unsupported file type: .$ext${NC}"
            ;;
    esac

    echo ""
    echo -e "  ${BOLD}Referenced by:${NC}"
    local basename_file
    basename_file=$(basename "$target_file" ".$ext")
    if command -v rg &>/dev/null; then
        rg -l "$basename_file" --glob '!node_modules' --glob '!.git' --glob '!dist' . 2>/dev/null | \
            grep -v "^${target_file}$" | head -20 | sed 's/^/  /'
    else
        grep -rl "$basename_file" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | \
            grep -v "^${target_file}$" | head -20 | sed 's/^/  /'
    fi
    echo ""
}

_code_hotspots() {
    local top_n=10
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --top) top_n="${2:-10}"; shift 2 ;;
            --top=*) top_n="${1#*=}"; shift ;;
            *) shift ;;
        esac
    done

    section_header "Code Hotspots (top $top_n)" 2>/dev/null || echo -e "\n${BOLD}Code Hotspots${NC}"

    if ! git rev-parse --is-inside-work-tree &>/dev/null; then
        echo -e "  ${RED}Not a git repository.${NC}"
        return 1
    fi

    echo -e "  ${BOLD}Most Changed Files (last 90 days):${NC}"
    echo ""
    git log --since="90 days ago" --pretty=format: --name-only 2>/dev/null | \
        sort | uniq -c | sort -rn | head -"$top_n" | \
        while read -r count file; do
            [ -z "$file" ] && continue
            printf "  %4d changes  %s\n" "$count" "$file"
        done

    echo ""
    echo -e "  ${BOLD}Largest Files by LOC:${NC}"
    echo ""
    find . -type f \( -name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.go' \
        -o -name '*.rs' -o -name '*.java' -o -name '*.sh' \) \
        -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' \
        -exec wc -l {} \; 2>/dev/null | sort -rn | head -"$top_n" | \
        while read -r lines file; do
            printf "  %6d lines  %s\n" "$lines" "$file"
        done
    echo ""
}

_code_diff() {
    if ! git rev-parse --is-inside-work-tree &>/dev/null; then
        echo -e "${RED}Not a git repository.${NC}"
        return 1
    fi

    # Use delta if available, otherwise colored diff
    if command -v delta &>/dev/null; then
        git diff "$@" | delta
    else
        git diff "$@" | while IFS= read -r line; do
            case "$line" in
                diff*)     echo -e "${BOLD}${line}${NC}" ;;
                ---*)      echo -e "${BOLD}${line}${NC}" ;;
                +++*)      echo -e "${BOLD}${line}${NC}" ;;
                @@*)       echo -e "${CYAN}${line}${NC}" ;;
                +*)        echo -e "${GREEN}${line}${NC}" ;;
                -*)        echo -e "${RED}${line}${NC}" ;;
                *)         echo "$line" ;;
            esac
        done
    fi
}

# Output shell completion scripts
cmd_completions() {
    local shell="${1:-bash}"
    # Consume the subcommand so the install arm can read an optional shell
    # override as its own positional ($1). bash/zsh raw-output arms ignore $@.
    shift 2>/dev/null || true
    local skill_dir

    # Find the skill directory (where autonomy/loki is located)
    skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
    local completions_dir="$skill_dir/completions"

    case "$shell" in
        bash)
            if [ -f "$completions_dir/loki.bash" ]; then
                cat "$completions_dir/loki.bash"
            else
                echo -e "${RED}Error: Bash completion script not found at $completions_dir/loki.bash${NC}" >&2
                exit 1
            fi
            ;;
        zsh)
            if [ -f "$completions_dir/_loki" ]; then
                cat "$completions_dir/_loki"
            else
                echo -e "${RED}Error: Zsh completion script not found at $completions_dir/_loki${NC}" >&2
                exit 1
            fi
            ;;
        install)
            # One-command install: detect the current shell, append a single
            # guarded line to the right rc file exactly once (idempotent via a
            # sentinel comment), then tell the user how to activate it. This
            # replaces the error-prone manual recipe with something that just
            # works after one command.
            local detected target_shell rc_file
            detected="$(basename "${SHELL:-}" 2>/dev/null)"
            # Allow an explicit override: loki completions install bash|zsh
            target_shell="${1:-$detected}"
            case "$target_shell" in
                bash)
                    rc_file="${HOME}/.bashrc"
                    ;;
                zsh)
                    rc_file="${HOME}/.zshrc"
                    ;;
                *)
                    echo -e "${RED}Could not detect a supported shell (got: ${target_shell:-unknown}).${NC}" >&2
                    echo "Specify one explicitly: loki completions install bash | loki completions install zsh" >&2
                    exit 1
                    ;;
            esac
            local sentinel="# loki-mode completions"
            if [ -f "$rc_file" ] && grep -qF "$sentinel" "$rc_file" 2>/dev/null; then
                echo "Loki completions already installed in ${rc_file} (no change)."
                echo "Activate now with: source ${rc_file}"
                return 0
            fi
            # Append the sentinel plus a single eval line that re-loads the
            # completion script at shell start. eval (not redirect) is the
            # correct persistent form: it sources the script into the
            # interactive shell each time the rc file runs.
            {
                printf '\n%s\n' "$sentinel"
                printf '%s\n' "eval \"\$(loki completions ${target_shell})\""
            } >> "$rc_file" || {
                echo -e "${RED}Failed to write to ${rc_file}.${NC}" >&2
                exit 1
            }
            echo "Installed Loki ${target_shell} completions into ${rc_file}."
            echo "Activate now with: source ${rc_file}  (or restart your shell)"
            return 0
            ;;
        *)
            echo -e "${BOLD}Loki Shell Completions${NC}"
            echo ""
            echo "Output shell completion scripts for bash or zsh."
            echo ""
            echo "Usage: loki completions <shell>"
            echo "       loki completions install [bash|zsh]"
            echo ""
            echo "Shells:"
            echo "  bash       Bash completion script"
            echo "  zsh        Zsh completion script"
            echo "  install    Auto-install completions into your shell rc file"
            echo ""
            echo "Installation:"
            echo ""
            echo "  One command (recommended):"
            echo "    loki completions install"
            echo ""
            echo "  Manual (persistent), Bash:"
            echo "    echo 'eval \"\$(loki completions bash)\"' >> ~/.bashrc"
            echo "  Manual (persistent), Zsh:"
            echo "    echo 'eval \"\$(loki completions zsh)\"' >> ~/.zshrc"
            echo ""
            echo "  Session only (current shell), Bash:"
            echo "    eval \"\$(loki completions bash)\""
            echo "  Session only (current shell), Zsh:"
            echo "    eval \"\$(loki completions zsh)\""
            echo ""
            exit 1
            ;;
    esac
}

cmd_template() {
    local subcommand="${1:-list}"
    shift 2>/dev/null || true

    local script_dir
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    local hub_py="${script_dir}/../agents/hub_install.py"
    [ -f "$hub_py" ] || hub_py="agents/hub_install.py"

    case "$subcommand" in
        install)
            local source="${1:-}"
            if [ -z "$source" ]; then
                echo -e "${RED}Usage: loki template install <source>${NC}"
                echo "  source: local path, git repo URL, or raw manifest URL"
                return 1
            fi
            if [ ! -f "$hub_py" ]; then
                echo -e "${RED}Error: agents/hub_install.py not found${NC}"
                return 1
            fi
            local result
            if result=$(python3 "$hub_py" install-template "$source" 2>&1); then
                TPL_RESULT="$result" python3 - << 'PYEOF'
import json, os
r = json.loads(os.environ["TPL_RESULT"])
print(f"Installed template: {r['name']}  ({r.get('label','')})")
desc = r.get("description") or ""
if desc:
    print(f"  {desc}")
print(f"  body:   {r.get('path','')}")
print(f"  source: {r.get('source','')}")
ig = r.get("_ignored_executable_fields") or []
if ig:
    print(f"  NOTE: ignored executable-looking fields (never run): {', '.join(ig)}")
print(f"Use with: loki init --template {r['name']}")
PYEOF
            else
                echo -e "${RED}Install failed:${NC} $result"
                return 1
            fi
            ;;

        list|installed)
            echo -e "${BOLD}Hub-installed Templates${NC}"
            echo ""
            if [ ! -f "$hub_py" ]; then
                echo "  (none)"
            else
                local _tpls_json
                _tpls_json="$(python3 "$hub_py" list-templates 2>/dev/null)"
                HUB_ITEMS="$_tpls_json" python3 - << 'PYEOF'
import json, os
try:
    items = json.loads(os.environ.get("HUB_ITEMS", "") or "[]")
except Exception:
    items = []
if not items:
    print("  (none)")
else:
    for t in items:
        print(f"  {t.get('name',''):24s} {t.get('label','')}")
    print(f"\n  Total: {len(items)} installed template(s)")
PYEOF
            fi
            ;;

        --help|-h|help)
            echo -e "${BOLD}loki template${NC} - PRD template marketplace (R10)"
            echo ""
            echo "Usage: loki template <command> [args]"
            echo ""
            echo "Commands:"
            echo "  install <source>   Install a community template (local/git/url manifest)"
            echo "  list               List templates installed from a hub source"
            echo "  help               Show this help"
            echo ""
            echo "Examples:"
            echo "  loki template install ./my-template/manifest.json"
            echo "  loki template install https://github.com/owner/loki-template-rust.git"
            echo "  loki template list"
            echo ""
            echo "Installed templates are usable via 'loki init --template <name>'."
            echo "Built-in templates ship in templates/ (see 'loki init --list')."
            echo ""
            echo "Note: install is install-from-source (git/local/url). A hosted"
            echo "central hub registry is future work. Manifests are validated as"
            echo "DATA only; no code from a manifest is ever executed."
            echo ""
            ;;
        *)
            echo -e "${RED}Unknown template command: $subcommand${NC}"
            echo "Run 'loki template help' for usage."
            return 1
            ;;
    esac
}

# User-configurable child agents: .loki/agents/<name>.md, ~/.config/loki/agents/<name>.md
cmd_agents() {
    local sub="${1:-list}"
    case "$sub" in
        help|-h|--help)
            echo "Usage: loki agents <list|new|show|export> [args]"
            echo "  list                    List configured agents (project overrides user)"
            echo "  new <name> [--user]     Scaffold .loki/agents/<name>.md (or ~/.config/loki/agents)"
            echo "                          Names reviewer, planner, tester copy the shipped examples"
            echo "  show <name>             Show one agent and its system prompt"
            echo "  export --claude [--user] Write agents as Claude Code subagents (.claude/agents/)"
            echo "Council reviewers use a configured agent named reviewer, tester or security."
            return 0
            ;;
    esac
    local script_dir helper
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    helper="${script_dir}/../agents/custom_agents.py"
    [ -f "$helper" ] || { echo -e "${RED}Error: agents/custom_agents.py not found${NC}"; return 1; }
    python3 -I "$helper" "$@"
}

# Agent type dispatch (v6.7.0)
cmd_agent() {
    local subcommand="${1:-list}"
    shift 2>/dev/null || true

    # Find agents/types.json relative to script location
    local script_dir
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    local types_file="${script_dir}/../agents/types.json"
    if [ ! -f "$types_file" ]; then
        # Try relative to current dir (development)
        types_file="agents/types.json"
    fi
    if [ ! -f "$types_file" ]; then
        echo -e "${RED}Error: agents/types.json not found${NC}"
        echo "Expected at: ${script_dir}/../agents/types.json"
        return 1
    fi

    case "$subcommand" in
        list)
            local filter_swarm="${1:-}"
            echo -e "${BOLD}Agent Types${NC}"
            echo ""
            TYPES_FILE="$types_file" FILTER_SWARM="$filter_swarm" FILTER_SWARM2="${2:-}" python3 << 'PYEOF'
import json, sys, os

with open(os.environ["TYPES_FILE"]) as f:
    agents = json.load(f)

# R10: union built-in agents with hub-installed agents (.loki/agents/installed.json).
try:
    import importlib.util as _ilu
    _hub_path = os.path.join(os.path.dirname(os.path.abspath(os.environ["TYPES_FILE"])), "hub_install.py")
    _spec = _ilu.spec_from_file_location("loki_hub_install", _hub_path)
    _hub = _ilu.module_from_spec(_spec)
    _spec.loader.exec_module(_hub)
    _seen = {a.get("type") for a in agents}
    for _inst in _hub.installed_agent_list():
        if _inst.get("type") not in _seen:
            agents.append(_inst)
except Exception:
    pass

filter_swarm = os.environ.get("FILTER_SWARM", "")
if filter_swarm and filter_swarm.startswith("--swarm"):
    # Handle --swarm=X or --swarm X
    if "=" in filter_swarm:
        filter_swarm = filter_swarm.split("=")[1]
    else:
        filter_swarm = os.environ.get("FILTER_SWARM2", "")

swarms = {}
for a in agents:
    s = a["swarm"]
    if filter_swarm and s != filter_swarm:
        continue
    swarms.setdefault(s, []).append(a)

total = 0
for swarm_name in sorted(swarms.keys()):
    print(f"  {swarm_name.upper()} SWARM:")
    for a in swarms[swarm_name]:
        caps = a.get("capabilities", "")
        if len(caps) > 60:
            caps = caps[:57] + "..."
        print(f"    {a['type']:22s} {caps}")
        total += 1
    print()

print(f"  Total: {total} agent type(s)")
PYEOF
            ;;

        info)
            local agent_type="${1:-}"
            if [ -z "$agent_type" ]; then
                echo -e "${RED}Usage: loki agent info <type>${NC}"
                return 1
            fi
            TYPES_FILE="$types_file" AGENT_TYPE="$agent_type" python3 << 'PYEOF'
import json, sys, os

with open(os.environ["TYPES_FILE"]) as f:
    agents = json.load(f)

# R10: include hub-installed agents in the lookup.
try:
    import importlib.util as _ilu
    _hub_path = os.path.join(os.path.dirname(os.path.abspath(os.environ["TYPES_FILE"])), "hub_install.py")
    _spec = _ilu.spec_from_file_location("loki_hub_install", _hub_path)
    _hub = _ilu.module_from_spec(_spec)
    _spec.loader.exec_module(_hub)
    _seen = {a.get("type") for a in agents}
    for _inst in _hub.installed_agent_list():
        if _inst.get("type") not in _seen:
            agents.append(_inst)
except Exception:
    pass

target = os.environ["AGENT_TYPE"]
found = None
for a in agents:
    if a["type"] == target:
        found = a
        break

if not found:
    print(f"Agent type not found: {target}")
    print("Run 'loki agent list' to see all types.")
    sys.exit(1)

print(f"Type:         {found['type']}")
print(f"Name:         {found['name']}")
print(f"Swarm:        {found['swarm']}")
print(f"Capabilities: {found['capabilities']}")
print(f"Focus:        {', '.join(found.get('focus', []))}")
print()
print(f"Persona:")
print(f"  {found['persona']}")
PYEOF
            ;;

        run)
            local agent_type="${1:-}"
            local prompt="${2:-}"
            if [ -z "$agent_type" ] || [ -z "$prompt" ]; then
                echo -e "${RED}Usage: loki agent run <type> \"<prompt>\"${NC}"
                return 1
            fi

            # Get persona from types.json
            local persona
            persona=$(AGENT_TYPE="$agent_type" TYPES_FILE="$types_file" python3 -c "
import json, os
with open(os.environ['TYPES_FILE']) as f:
    agents = json.load(f)
try:
    import importlib.util as _ilu
    _hp = os.path.join(os.path.dirname(os.path.abspath(os.environ['TYPES_FILE'])), 'hub_install.py')
    _sp = _ilu.spec_from_file_location('loki_hub_install', _hp)
    _hub = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_hub)
    _seen = {a.get('type') for a in agents}
    for _i in _hub.installed_agent_list():
        if _i.get('type') not in _seen:
            agents.append(_i)
except Exception:
    pass
for a in agents:
    if a['type'] == os.environ['AGENT_TYPE']:
        print(a['persona'])
        break
" 2>/dev/null)

            if [ -z "$persona" ]; then
                echo -e "${RED}Unknown agent type: $agent_type${NC}"
                return 1
            fi

            # BUG-PU-003: Separate persona from user prompt with clear delimiter
            # so the AI provider can distinguish role instruction from task
            local full_prompt="You are acting as the following specialist agent:

${persona}

---

USER TASK: ${prompt}"
            echo -e "${BOLD}Running as: $agent_type${NC}"
            echo -e "${DIM}Persona: ${persona:0:80}...${NC}"
            echo ""

            # Invoke current provider and capture exit code (#72)
            local provider="${LOKI_PROVIDER:-claude}"
            if [ -f ".loki/state/provider" ]; then
                provider=$(cat ".loki/state/provider" 2>/dev/null)
            fi

            local agent_exit=0
            case "$provider" in
                claude)
                    _loki_perm_argv_for "$PWD" >/dev/null 2>&1 || true
                    SHELL="$_LOKI_CLAUDE_SHELL" claude -p "$full_prompt" 2>&1 || agent_exit=$?
                    ;;
                codex)
                    codex exec --sandbox workspace-write "$full_prompt" 2>&1 || agent_exit=$?
                    ;;
                cline)
                    cline -y "$full_prompt" 2>&1 || agent_exit=$?
                    ;;
                aider)
                    aider --message "$full_prompt" --yes-always --no-auto-commits < /dev/null 2>&1 || agent_exit=$?
                    ;;
                opencode)
                    # Mirrors the working arm in the phase dispatcher (see the
                    # opencode case alongside aider there). opencode was listed
                    # as an active provider and accepted by `loki provider set`,
                    # but this case had no arm, so `loki agent run` printed the
                    # persona banner and then died with "Unknown provider".
                    (source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$full_prompt" 2>&1) || agent_exit=$?
                    ;;
                *)
                    echo -e "${RED}Unknown provider: $provider${NC}"
                    return 1
                    ;;
            esac

            if [ "$agent_exit" -ne 0 ]; then
                echo ""
                echo -e "${RED}Agent exited with code $agent_exit${NC}"
                return "$agent_exit"
            fi
            ;;

        start)
            local agent_type="${1:-}"
            local prd="${2:-}"
            if [ -z "$agent_type" ] || [ -z "$prd" ]; then
                echo -e "${RED}Usage: loki agent start <type> \"<prompt>\" or loki agent start <type> <prd-file>${NC}"
                return 1
            fi

            # Get persona
            local persona
            persona=$(AGENT_TYPE="$agent_type" TYPES_FILE="$types_file" python3 -c "
import json, os
with open(os.environ['TYPES_FILE']) as f:
    agents = json.load(f)
try:
    import importlib.util as _ilu
    _hp = os.path.join(os.path.dirname(os.path.abspath(os.environ['TYPES_FILE'])), 'hub_install.py')
    _sp = _ilu.spec_from_file_location('loki_hub_install', _hp)
    _hub = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_hub)
    _seen = {a.get('type') for a in agents}
    for _i in _hub.installed_agent_list():
        if _i.get('type') not in _seen:
            agents.append(_i)
except Exception:
    pass
for a in agents:
    if a['type'] == os.environ['AGENT_TYPE']:
        print(a['persona'])
        break
" 2>/dev/null)

            if [ -z "$persona" ]; then
                echo -e "${RED}Unknown agent type: $agent_type${NC}"
                return 1
            fi

            echo -e "${BOLD}Starting autonomous session as: $agent_type${NC}"

            # Set agent persona as env var for run.sh to pick up
            export LOKI_AGENT_PERSONA="$persona"
            export LOKI_AGENT_TYPE="$agent_type"

            # Delegate to regular start
            if [ -f "$prd" ]; then
                cmd_start "$prd"
            else
                # Treat as inline prompt - create temp PRD
                # BUG-PU-003: Use .loki/ directory instead of /tmp so run.sh
                # can find it after exec replaces this process. The temp file
                # in /tmp was never cleaned because cmd_start uses exec.
                mkdir -p "$LOKI_DIR"
                local tmp_prd="$LOKI_DIR/agent-prd-$$.md"
                echo "# Agent Task: $agent_type" > "$tmp_prd"
                echo "" >> "$tmp_prd"
                echo "$prd" >> "$tmp_prd"
                cmd_start "$tmp_prd"
            fi
            ;;

        review)
            local agent_type="${1:-ops-security}"
            echo -e "${BOLD}Code review as: $agent_type${NC}"

            local persona
            persona=$(AGENT_TYPE="$agent_type" TYPES_FILE="$types_file" python3 -c "
import json, os
with open(os.environ['TYPES_FILE']) as f:
    agents = json.load(f)
try:
    import importlib.util as _ilu
    _hp = os.path.join(os.path.dirname(os.path.abspath(os.environ['TYPES_FILE'])), 'hub_install.py')
    _sp = _ilu.spec_from_file_location('loki_hub_install', _hp)
    _hub = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_hub)
    _seen = {a.get('type') for a in agents}
    for _i in _hub.installed_agent_list():
        if _i.get('type') not in _seen:
            agents.append(_i)
except Exception:
    pass
for a in agents:
    if a['type'] == os.environ['AGENT_TYPE']:
        print(a['persona'])
        break
" 2>/dev/null)

            if [ -z "$persona" ]; then
                echo -e "${RED}Unknown agent type: $agent_type${NC}"
                return 1
            fi

            local diff
            diff=$(git diff HEAD~1 2>/dev/null || git diff --cached 2>/dev/null || echo "No changes to review")

            local review_prompt="$persona

Review the following code changes. Focus on your area of expertise.
Provide findings as:
- CRITICAL: Must fix before merge
- HIGH: Should fix
- MEDIUM: Improvement suggestion
- LOW: Nitpick

Changes:
$diff"

            local provider="${LOKI_PROVIDER:-claude}"
            [ -f ".loki/state/provider" ] && provider=$(cat ".loki/state/provider" 2>/dev/null)

            case "$provider" in
                claude)  _loki_perm_argv_for "$PWD" >/dev/null 2>&1 || true; SHELL="$_LOKI_CLAUDE_SHELL" claude -p "$review_prompt" 2>&1 ;;
                codex)   codex exec --sandbox workspace-write "$review_prompt" 2>&1 ;;
                cline)   cline -y "$review_prompt" 2>&1 ;;
                aider)   aider --message "$review_prompt" --yes-always --no-auto-commits < /dev/null 2>&1 ;;
                opencode) (source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$review_prompt" 2>&1) ;;
                *)       echo -e "${RED}Unknown provider: $provider${NC}"; return 1 ;;
            esac
            ;;

        install)
            # R10: install a community agent from a source (local path /
            # git repo / raw URL containing a manifest). DATA-ONLY install --
            # never executes code from the manifest. See agents/hub_install.py.
            local source="${1:-}"
            if [ -z "$source" ]; then
                echo -e "${RED}Usage: loki agent install <source>${NC}"
                echo "  source: local path, git repo URL, or raw manifest URL"
                return 1
            fi
            local hub_py="${script_dir}/../agents/hub_install.py"
            [ -f "$hub_py" ] || hub_py="agents/hub_install.py"
            if [ ! -f "$hub_py" ]; then
                echo -e "${RED}Error: agents/hub_install.py not found${NC}"
                return 1
            fi
            local result
            if result=$(python3 "$hub_py" install-agent "$source" 2>&1); then
                AGENT_RESULT="$result" python3 - << 'PYEOF'
import json, os
r = json.loads(os.environ["AGENT_RESULT"])
print(f"Installed agent: {r['type']}  ({r.get('name','')})")
print(f"  swarm:   {r.get('swarm','')}")
print(f"  source:  {r.get('source','')}")
ig = r.get("_ignored_executable_fields") or []
if ig:
    print(f"  NOTE: ignored executable-looking fields (never run): {', '.join(ig)}")
print("Stored in .loki/agents/installed.json -- visible to 'loki agent list/info/run'.")
print("It also joins the code-review reviewer pool: when its focus keywords match")
print("a diff it is dispatched as a real reviewer (appended to the built-in battery,")
print("never replacing one). See skills/quality-gates.md 'Adding Your Own Reviewer'.")
PYEOF
            else
                echo -e "${RED}Install failed:${NC} $result"
                return 1
            fi
            ;;

        installed)
            # R10: list agents installed from a hub source.
            local hub_py="${script_dir}/../agents/hub_install.py"
            [ -f "$hub_py" ] || hub_py="agents/hub_install.py"
            echo -e "${BOLD}Hub-installed Agents${NC}"
            echo ""
            if [ ! -f "$hub_py" ]; then
                echo "  (none)"
            else
                local _agents_json
                _agents_json="$(python3 "$hub_py" list-agents 2>/dev/null)"
                HUB_ITEMS="$_agents_json" python3 - << 'PYEOF'
import json, os
try:
    items = json.loads(os.environ.get("HUB_ITEMS", "") or "[]")
except Exception:
    items = []
if not items:
    print("  (none)")
else:
    for a in items:
        print(f"  {a.get('type',''):24s} {a.get('name','')}  [{a.get('swarm','')}]")
    print(f"\n  Total: {len(items)} installed agent(s)")
PYEOF
            fi
            ;;

        --help|-h|help)
            echo -e "${BOLD}loki agent${NC} - Agent type dispatch"
            echo ""
            echo "Usage: loki agent <command> [args]"
            echo ""
            echo "Commands:"
            echo "  list [--swarm NAME]        List all agent types (built-in + installed)"
            echo "  info <type>                Show agent type details"
            echo "  run <type> \"<prompt>\"       Single-shot: run prompt with agent persona"
            echo "  start <type> <prd|prompt>  Full autonomous loop with agent persona"
            echo "  review [type]              Code review with agent persona (default: ops-security)"
            echo "  install <source>           Install a community agent (local/git/url manifest)"
            echo "  installed                  List agents installed from a hub source"
            echo "  help                       Show this help"
            echo ""
            echo "Examples:"
            echo "  loki agent list"
            echo "  loki agent list --swarm engineering"
            echo "  loki agent info eng-frontend"
            echo "  loki agent run eng-frontend \"Create a responsive navbar\""
            echo "  loki agent start eng-backend ./api-prd.md"
            echo "  loki agent review ops-security"
            echo "  loki agent install ./my-agent/manifest.json"
            echo "  loki agent install https://github.com/owner/loki-agent-rust.git"
            echo ""
            echo "Note: 'install' is install-from-source (git/local/url). A hosted"
            echo "central hub registry is future work. Manifests are validated as"
            echo "DATA only; no code from a manifest is ever executed."
            echo ""
            ;;
        *)
            echo -e "${RED}Unknown agent command: $subcommand${NC}"
            echo "Run 'loki agent help' for usage."
            return 1
            ;;
    esac
}

# Project onboarding - analyze repo and generate CLAUDE.md (v6.21.0)
cmd_onboard() {
    local target_path="."
    local depth=2
    local format="markdown"
    local output_path=""
    local use_stdout=false
    local update_mode=false

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --depth)
                depth="${2:-2}"
                shift 2
                ;;
            --format)
                format="${2:-markdown}"
                shift 2
                ;;
            --output)
                output_path="${2:-}"
                shift 2
                ;;
            --stdout)
                use_stdout=true
                shift
                ;;
            --update)
                update_mode=true
                shift
                ;;
            --help|-h)
                echo -e "${BOLD}loki onboard${NC} - Analyze a project and generate CLAUDE.md"
                echo ""
                echo "Usage: loki onboard [path] [options]"
                echo ""
                echo "Arguments:"
                echo "  path              Path to repository (default: current directory)"
                echo ""
                echo "Options:"
                echo "  --depth N         Analysis depth: 1=surface, 2=moderate, 3=deep (default: 2)"
                echo "  --format FORMAT   Output format: markdown, json, yaml (default: markdown)"
                echo "  --output PATH     Custom output file path"
                echo "  --stdout          Print to stdout instead of writing file"
                echo "  --update          Update existing CLAUDE.md with new findings"
                echo "  --help            Show this help"
                echo ""
                echo "Examples:"
                echo "  loki onboard                    # Analyze current directory"
                echo "  loki onboard ~/projects/myapp   # Analyze specific repo"
                echo "  loki onboard --depth 3          # Deep analysis with dependency mapping"
                echo "  loki onboard --format json      # JSON output"
                echo "  loki onboard --stdout            # Print to terminal"
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                echo "Run 'loki onboard --help' for usage."
                return 1
                ;;
            *)
                target_path="$1"
                shift
                ;;
        esac
    done

    # Validate target path
    if [ ! -d "$target_path" ]; then
        log_error "Directory not found: $target_path"
        return 1
    fi

    # Resolve to absolute path
    target_path="$(cd "$target_path" && pwd)"

    # When --stdout, send log messages to stderr to keep stdout clean
    if [ "$use_stdout" = true ]; then
        log_info "Analyzing project at: $target_path" >&2
        log_info "Depth: $depth | Format: $format" >&2
    else
        log_info "Analyzing project at: $target_path"
        log_info "Depth: $depth | Format: $format"
    fi

    # --- Detect project metadata ---
    local project_name
    project_name="$(basename "$target_path")"

    local languages=""
    local frameworks=""
    local build_system=""
    local test_framework=""
    local entry_points=""
    local package_manager=""
    local project_description=""
    local project_version=""

    # Detect languages and config files
    local config_files=""

    if [ -f "$target_path/package.json" ]; then
        config_files="$config_files package.json"
        languages="$languages JavaScript/TypeScript"
        package_manager="npm"
        if [ -f "$target_path/yarn.lock" ]; then
            package_manager="yarn"
        elif [ -f "$target_path/pnpm-lock.yaml" ]; then
            package_manager="pnpm"
        elif [ -f "$target_path/bun.lockb" ]; then
            package_manager="bun"
        fi
        # Extract metadata from package.json
        if command -v python3 &>/dev/null; then
            # WAVE8 loki-est (same class as cmd_explain): pass the repo path via
            # env (os.environ) instead of interpolating into the python source.
            # A path with an apostrophe made each heredoc a SyntaxError, silently
            # dropping the package.json name/version/description under `|| true`.
            local pkg_name
            pkg_name=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['LOKI_ONB_PKG']))
    print(d.get('name', ''))
except: pass
" 2>/dev/null || true)
            if [ -n "$pkg_name" ]; then
                project_name="$pkg_name"
            fi
            project_description=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['LOKI_ONB_PKG']))
    print(d.get('description', ''))
except: pass
" 2>/dev/null || true)
            project_version=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['LOKI_ONB_PKG']))
    print(d.get('version', ''))
except: pass
" 2>/dev/null || true)
            entry_points=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['LOKI_ONB_PKG']))
    main = d.get('main', '')
    if main: print(main)
    scripts = d.get('scripts', {})
    if 'start' in scripts: print('scripts.start: ' + scripts['start'])
except: pass
" 2>/dev/null || true)
        fi
        # Detect frameworks from dependencies
        if grep -q '"react"' "$target_path/package.json" 2>/dev/null; then
            frameworks="$frameworks React"
        fi
        if grep -q '"next"' "$target_path/package.json" 2>/dev/null; then
            frameworks="$frameworks Next.js"
        fi
        if grep -q '"vue"' "$target_path/package.json" 2>/dev/null; then
            frameworks="$frameworks Vue"
        fi
        if grep -q '"express"' "$target_path/package.json" 2>/dev/null; then
            frameworks="$frameworks Express"
        fi
        if grep -q '"fastify"' "$target_path/package.json" 2>/dev/null; then
            frameworks="$frameworks Fastify"
        fi
        if grep -q '"svelte"' "$target_path/package.json" 2>/dev/null; then
            frameworks="$frameworks Svelte"
        fi
        # Detect test framework
        if grep -q '"jest"' "$target_path/package.json" 2>/dev/null; then
            test_framework="$test_framework jest"
        fi
        if grep -q '"vitest"' "$target_path/package.json" 2>/dev/null; then
            test_framework="$test_framework vitest"
        fi
        if grep -q '"mocha"' "$target_path/package.json" 2>/dev/null; then
            test_framework="$test_framework mocha"
        fi
        if grep -q '"playwright"' "$target_path/package.json" 2>/dev/null; then
            test_framework="$test_framework playwright"
        fi
    fi

    if [ -f "$target_path/pyproject.toml" ]; then
        config_files="$config_files pyproject.toml"
        languages="$languages Python"
        package_manager="pip/poetry"
        if grep -q "django" "$target_path/pyproject.toml" 2>/dev/null; then
            frameworks="$frameworks Django"
        fi
        if grep -q "flask" "$target_path/pyproject.toml" 2>/dev/null; then
            frameworks="$frameworks Flask"
        fi
        if grep -q "fastapi" "$target_path/pyproject.toml" 2>/dev/null; then
            frameworks="$frameworks FastAPI"
        fi
        if grep -q "pytest" "$target_path/pyproject.toml" 2>/dev/null; then
            test_framework="$test_framework pytest"
        fi
    fi

    if [ -f "$target_path/setup.py" ] || [ -f "$target_path/setup.cfg" ]; then
        config_files="$config_files setup.py"
        languages="$languages Python"
        [ -z "$package_manager" ] && package_manager="pip"
    fi

    if [ -f "$target_path/requirements.txt" ]; then
        config_files="$config_files requirements.txt"
        languages="$languages Python"
        [ -z "$package_manager" ] && package_manager="pip"
    fi

    if [ -f "$target_path/Cargo.toml" ]; then
        config_files="$config_files Cargo.toml"
        languages="$languages Rust"
        package_manager="cargo"
        build_system="cargo"
        test_framework="$test_framework cargo-test"
    fi

    if [ -f "$target_path/go.mod" ]; then
        config_files="$config_files go.mod"
        languages="$languages Go"
        package_manager="go-modules"
        build_system="go"
        test_framework="$test_framework go-test"
    fi

    if [ -f "$target_path/Gemfile" ]; then
        config_files="$config_files Gemfile"
        languages="$languages Ruby"
        package_manager="bundler"
        if grep -q "rails" "$target_path/Gemfile" 2>/dev/null; then
            frameworks="$frameworks Rails"
        fi
        if grep -q "rspec" "$target_path/Gemfile" 2>/dev/null; then
            test_framework="$test_framework rspec"
        fi
    fi

    if [ -f "$target_path/pom.xml" ]; then
        config_files="$config_files pom.xml"
        languages="$languages Java"
        build_system="maven"
        package_manager="maven"
    fi

    if [ -f "$target_path/build.gradle" ] || [ -f "$target_path/build.gradle.kts" ]; then
        config_files="$config_files build.gradle"
        languages="$languages Java/Kotlin"
        build_system="gradle"
        package_manager="gradle"
    fi

    if [ -f "$target_path/Makefile" ]; then
        config_files="$config_files Makefile"
        [ -z "$build_system" ] && build_system="make"
    fi

    if [ -f "$target_path/CMakeLists.txt" ]; then
        config_files="$config_files CMakeLists.txt"
        languages="$languages C/C++"
        build_system="cmake"
    fi

    # Detect shell scripts
    local shell_count=0
    shell_count=$(find "$target_path" -maxdepth 2 -name "*.sh" -type f 2>/dev/null | wc -l | tr -d ' ' || true)
    if [ "$shell_count" -gt 0 ]; then
        languages="$languages Bash"
    fi

    # Deduplicate languages
    languages=$(echo "$languages" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')
    frameworks=$(echo "$frameworks" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')
    test_framework=$(echo "$test_framework" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')

    # --- Detect CI/CD ---
    local ci_system=""
    if [ -d "$target_path/.github/workflows" ]; then
        ci_system="GitHub Actions"
    fi
    if [ -f "$target_path/.gitlab-ci.yml" ]; then
        ci_system="$ci_system GitLab CI"
    fi
    if [ -f "$target_path/Jenkinsfile" ]; then
        ci_system="$ci_system Jenkins"
    fi
    if [ -f "$target_path/.circleci/config.yml" ]; then
        ci_system="$ci_system CircleCI"
    fi
    if [ -f "$target_path/.travis.yml" ]; then
        ci_system="$ci_system Travis CI"
    fi

    # --- Read README ---
    local readme_content=""
    local readme_file=""
    for f in README.md readme.md README.rst README README.txt; do
        if [ -f "$target_path/$f" ]; then
            readme_file="$f"
            readme_content=$(head -50 "$target_path/$f" 2>/dev/null || true)
            break
        fi
    done

    # Extract first meaningful line from README as description fallback
    if [ -z "$project_description" ] && [ -n "$readme_content" ]; then
        project_description=$(echo "$readme_content" | grep -v '^#' | grep -v '^$' | grep -v '^\[' | grep -v '^!' | head -1 | sed 's/^ *//')
    fi

    # --- Build directory tree ---
    local tree_output=""
    # -e not -d: in a git WORKTREE, .git is a pointer FILE, not a directory.
    # -d sent every worktree down the find fallback below.
    if command -v git &>/dev/null && [ -e "$target_path/.git" ]; then
        # Use git ls-files for accurate tree (respects .gitignore)
        tree_output=$(cd "$target_path" && git ls-files 2>/dev/null | head -200 || true)
    else
        # Fallback: find with common excludions.
        # `|| true` is load-bearing under `set -euo pipefail` (line 22): head -200
        # exits after 200 lines, find keeps writing, gets SIGPIPE, and the pipeline
        # returns 141 -- which -e turned into a silent abort producing 0 bytes on
        # any repo with more than 200 files. The git branch above is already guarded.
        tree_output=$(find "$target_path" -maxdepth 4 -type f \
            -not -path '*/node_modules/*' \
            -not -path '*/.git/*' \
            -not -path '*/vendor/*' \
            -not -path '*/__pycache__/*' \
            -not -path '*/dist/*' \
            -not -path '*/build/*' \
            -not -path '*/.next/*' \
            -not -path '*/target/*' \
            2>/dev/null | sed "s|$target_path/||" | sort | head -200 || true)
    fi

    # Categorize files
    local src_files=""
    local test_files=""
    local doc_files=""
    local config_file_list=""
    local ci_files=""
    local other_files=""

    while IFS= read -r file; do
        [ -z "$file" ] && continue
        case "$file" in
            *.test.*|*.spec.*|*_test.*|*_spec.*|tests/*|test/*|__tests__/*|spec/*)
                test_files="$test_files $file"
                ;;
            *.md|*.rst|*.txt|docs/*|doc/*|wiki/*)
                doc_files="$doc_files $file"
                ;;
            .github/*|.gitlab-ci*|.circleci/*|Jenkinsfile|.travis*)
                ci_files="$ci_files $file"
                ;;
            package.json|pyproject.toml|Cargo.toml|go.mod|Gemfile|pom.xml|build.gradle*|Makefile|CMakeLists.txt|*.toml|*.cfg|*.ini|*.yml|*.yaml|Dockerfile*|docker-compose*|.env*|.eslint*|.prettier*|tsconfig*|jest.config*|vitest.config*)
                config_file_list="$config_file_list $file"
                ;;
            *.js|*.ts|*.tsx|*.jsx|*.py|*.rs|*.go|*.rb|*.java|*.kt|*.c|*.cpp|*.h|*.hpp|*.sh|*.swift|*.cs|*.php|*.lua|*.zig|*.el|*.clj)
                src_files="$src_files $file"
                ;;
            *)
                other_files="$other_files $file"
                ;;
        esac
    done <<< "$tree_output"

    local src_count=$(echo "$src_files" | wc -w | tr -d ' ')
    local test_count=$(echo "$test_files" | wc -w | tr -d ' ')
    local doc_count=$(echo "$doc_files" | wc -w | tr -d ' ')

    # --- Depth 2+: Analyze source files ---
    local key_exports=""
    local key_functions=""
    local key_classes=""

    if [ "$depth" -ge 2 ]; then
        if [ "$use_stdout" = true ]; then
            log_info "Depth 2: Scanning source files for exports, functions, classes..." >&2
        else
            log_info "Depth 2: Scanning source files for exports, functions, classes..."
        fi

        for src_file in $src_files; do
            [ ! -f "$target_path/$src_file" ] && continue
            local file_lines
            file_lines=$(wc -l < "$target_path/$src_file" 2>/dev/null | tr -d ' ')

            # Only scan files under 2000 lines for performance
            [ "$file_lines" -gt 2000 ] && continue

            local file_ext="${src_file##*.}"
            case "$file_ext" in
                js|ts|tsx|jsx)
                    # Find exported functions/classes
                    local exports
                    exports=$(grep -n "^export " "$target_path/$src_file" 2>/dev/null | head -10 || true)
                    if [ -n "$exports" ]; then
                        key_exports="$key_exports
$src_file:
$exports"
                    fi
                    ;;
                py)
                    # Find class and function definitions
                    local classes
                    classes=$(grep -n "^class " "$target_path/$src_file" 2>/dev/null | head -5 || true)
                    local funcs
                    funcs=$(grep -n "^def \|^async def " "$target_path/$src_file" 2>/dev/null | head -10 || true)
                    if [ -n "$classes" ]; then
                        key_classes="$key_classes
$src_file:
$classes"
                    fi
                    if [ -n "$funcs" ]; then
                        key_functions="$key_functions
$src_file:
$funcs"
                    fi
                    ;;
                go)
                    local funcs
                    funcs=$(grep -n "^func " "$target_path/$src_file" 2>/dev/null | head -10 || true)
                    if [ -n "$funcs" ]; then
                        key_functions="$key_functions
$src_file:
$funcs"
                    fi
                    ;;
                rs)
                    local funcs
                    funcs=$(grep -n "^pub fn \|^pub async fn " "$target_path/$src_file" 2>/dev/null | head -10 || true)
                    if [ -n "$funcs" ]; then
                        key_functions="$key_functions
$src_file:
$funcs"
                    fi
                    ;;
                sh)
                    local funcs
                    funcs=$(grep -n "^[a-zA-Z_][a-zA-Z_0-9]*() {" "$target_path/$src_file" 2>/dev/null | head -10 || true)
                    if [ -n "$funcs" ]; then
                        key_functions="$key_functions
$src_file:
$funcs"
                    fi
                    ;;
                rb)
                    local classes
                    classes=$(grep -n "^class " "$target_path/$src_file" 2>/dev/null | head -5 || true)
                    local funcs
                    funcs=$(grep -n "^  def " "$target_path/$src_file" 2>/dev/null | head -10 || true)
                    if [ -n "$classes" ]; then
                        key_classes="$key_classes
$src_file:
$classes"
                    fi
                    if [ -n "$funcs" ]; then
                        key_functions="$key_functions
$src_file:
$funcs"
                    fi
                    ;;
                java|kt)
                    local classes
                    classes=$(grep -n "^public class \|^class \|^data class " "$target_path/$src_file" 2>/dev/null | head -5 || true)
                    if [ -n "$classes" ]; then
                        key_classes="$key_classes
$src_file:
$classes"
                    fi
                    ;;
            esac
        done
    fi

    # --- Depth 3: Dependency analysis ---
    local dep_graph=""

    if [ "$depth" -ge 3 ]; then
        if [ "$use_stdout" = true ]; then
            log_info "Depth 3: Analyzing imports and dependencies..." >&2
        else
            log_info "Depth 3: Analyzing imports and dependencies..."
        fi

        for src_file in $src_files; do
            [ ! -f "$target_path/$src_file" ] && continue
            local file_lines
            file_lines=$(wc -l < "$target_path/$src_file" 2>/dev/null | tr -d ' ')
            [ "$file_lines" -gt 2000 ] && continue

            local imports=""
            local file_ext="${src_file##*.}"
            case "$file_ext" in
                js|ts|tsx|jsx)
                    imports=$(grep "^import " "$target_path/$src_file" 2>/dev/null | grep -v "node_modules" | head -15 || true)
                    ;;
                py)
                    imports=$(grep "^import \|^from " "$target_path/$src_file" 2>/dev/null | head -15 || true)
                    ;;
                go)
                    imports=$(sed -n '/^import (/,/^)/p' "$target_path/$src_file" 2>/dev/null | grep -v '^import\|^)' | head -15 || true)
                    ;;
                rs)
                    imports=$(grep "^use " "$target_path/$src_file" 2>/dev/null | head -15 || true)
                    ;;
            esac

            if [ -n "$imports" ]; then
                dep_graph="$dep_graph
$src_file:
$imports"
            fi
        done
    fi

    # --- Detect build/run/test commands ---
    local build_cmd=""
    local run_cmd=""
    local test_cmd=""

    if [ -f "$target_path/package.json" ]; then
        if command -v python3 &>/dev/null; then
            local scripts_json
            # WAVE8 loki-est: env-passed path (see cmd_onboard metadata block).
            scripts_json=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['LOKI_ONB_PKG']))
    s = d.get('scripts', {})
    for k in ['build', 'dev', 'start', 'test', 'lint', 'format', 'check', 'typecheck']:
        if k in s:
            print(f'{k}: {s[k]}')
except: pass
" 2>/dev/null || true)
            if echo "$scripts_json" | grep -q "^build:"; then
                build_cmd="${package_manager:-npm} run build"
            fi
            if echo "$scripts_json" | grep -q "^dev:"; then
                run_cmd="${package_manager:-npm} run dev"
            elif echo "$scripts_json" | grep -q "^start:"; then
                run_cmd="${package_manager:-npm} start"
            fi
            if echo "$scripts_json" | grep -q "^test:"; then
                test_cmd="${package_manager:-npm} test"
            fi
        fi
    fi

    if [ -f "$target_path/Makefile" ]; then
        [ -z "$build_cmd" ] && build_cmd="make"
        if grep -q "^test:" "$target_path/Makefile" 2>/dev/null; then
            [ -z "$test_cmd" ] && test_cmd="make test"
        fi
        if grep -q "^run:" "$target_path/Makefile" 2>/dev/null; then
            [ -z "$run_cmd" ] && run_cmd="make run"
        fi
    fi

    if [ -f "$target_path/Cargo.toml" ]; then
        build_cmd="cargo build"
        run_cmd="cargo run"
        test_cmd="cargo test"
    fi

    if [ -f "$target_path/go.mod" ]; then
        build_cmd="go build ./..."
        run_cmd="go run ."
        test_cmd="go test ./..."
    fi

    if [ -f "$target_path/pyproject.toml" ]; then
        if grep -q '\[tool.pytest' "$target_path/pyproject.toml" 2>/dev/null; then
            test_cmd="pytest"
        fi
        if grep -q '\[tool.poetry' "$target_path/pyproject.toml" 2>/dev/null; then
            build_cmd="poetry build"
            run_cmd="poetry run python -m ${project_name}"
        fi
    fi

    # --- Generate output ---
    local output=""

    if [ "$format" = "json" ]; then
        # JSON output
        output=$(cat <<ENDJSON
{
  "project": {
    "name": $(_VAL="$project_name" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_name\""),
    "description": $(_DESC="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_DESC','')))" 2>/dev/null || echo "\"$project_description\""),
    "version": $(_VAL="$project_version" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_version\""),
    "path": $(_VAL="$target_path" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$target_path\"")
  },
  "languages": "$(echo $languages | sed 's/  */ /g')",
  "frameworks": "$(echo $frameworks | sed 's/  */ /g')",
  "build_system": "$build_system",
  "package_manager": "$package_manager",
  "test_framework": "$(echo $test_framework | sed 's/  */ /g')",
  "ci": "$(echo $ci_system | sed 's/  */ /g')",
  "files": {
    "source": $src_count,
    "test": $test_count,
    "docs": $doc_count
  },
  "commands": {
    "build": $(_VAL="$build_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$build_cmd\""),
    "run": $(_VAL="$run_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$run_cmd\""),
    "test": $(_VAL="$test_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$test_cmd\"")
  },
  "depth": $depth
}
ENDJSON
)
    elif [ "$format" = "yaml" ]; then
        # YAML output
        output=$(cat <<ENDYAML
project:
  name: $(_VAL="$project_name" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_name\"")
  description: $(_VAL="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_description\"")
  version: $(_VAL="$project_version" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_version\"")
  path: $(_VAL="$target_path" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$target_path\"")
languages: $languages
frameworks: $frameworks
build_system: $build_system
package_manager: $package_manager
test_framework: $(echo $test_framework | sed 's/  */ /g')
ci: $(echo $ci_system | sed 's/  */ /g')
files:
  source: $src_count
  test: $test_count
  docs: $doc_count
commands:
  build: "$build_cmd"
  run: "$run_cmd"
  test: "$test_cmd"
depth: $depth
ENDYAML
)
    else
        # Markdown output (CLAUDE.md format)
        output="# $project_name"
        [ -n "$project_description" ] && output="$output

$project_description"
        [ -n "$project_version" ] && output="$output

Version: $project_version"

        output="$output

## Overview

| Property | Value |
|----------|-------|
| Languages | ${languages:-N/A} |
| Frameworks | ${frameworks:-N/A} |
| Build System | ${build_system:-N/A} |
| Package Manager | ${package_manager:-N/A} |
| Test Framework | ${test_framework:-N/A} |
| CI/CD | ${ci_system:-N/A} |"

        # Commands section
        if [ -n "$build_cmd" ] || [ -n "$run_cmd" ] || [ -n "$test_cmd" ]; then
            output="$output

## Commands

\`\`\`bash"
            [ -n "$build_cmd" ] && output="$output
# Build
$build_cmd"
            [ -n "$run_cmd" ] && output="$output

# Run
$run_cmd"
            [ -n "$test_cmd" ] && output="$output

# Test
$test_cmd"
            output="$output
\`\`\`"
        fi

        # Project structure
        output="$output

## Project Structure

Files: $src_count source, $test_count test, $doc_count docs"

        # Show directory structure (top-level)
        local top_dirs
        top_dirs=$(echo "$tree_output" | sed 's|/.*||' | sort -u | head -30)
        if [ -n "$top_dirs" ]; then
            output="$output

\`\`\`"
            while IFS= read -r dir; do
                [ -z "$dir" ] && continue
                if [ -d "$target_path/$dir" ]; then
                    # Count files in directory
                    local dir_count
                    dir_count=$(echo "$tree_output" | grep "^${dir}/" | wc -l | tr -d ' ' || true)
                    output="$output
$dir/    ($dir_count files)"
                else
                    output="$output
$dir"
                fi
            done <<< "$top_dirs"
            output="$output
\`\`\`"
        fi

        # Key files
        if [ -n "$config_file_list" ]; then
            output="$output

## Key Files
"
            for cf in $config_file_list; do
                output="$output
- \`$cf\`"
            done
        fi

        if [ -n "$entry_points" ]; then
            output="$output

## Entry Points

\`\`\`
$entry_points
\`\`\`"
        fi

        # Depth 2+: exports, functions, classes
        if [ "$depth" -ge 2 ]; then
            if [ -n "$key_classes" ]; then
                output="$output

## Key Classes
\`\`\`
$key_classes
\`\`\`"
            fi

            if [ -n "$key_functions" ]; then
                output="$output

## Key Functions
\`\`\`
$key_functions
\`\`\`"
            fi

            if [ -n "$key_exports" ]; then
                output="$output

## Exports
\`\`\`
$key_exports
\`\`\`"
            fi
        fi

        # Depth 3: dependency graph
        if [ "$depth" -ge 3 ] && [ -n "$dep_graph" ]; then
            output="$output

## Dependency Graph (Imports)
\`\`\`
$dep_graph
\`\`\`"
        fi

        # CI/CD section
        if [ -n "$ci_system" ] && [ -n "$ci_files" ]; then
            output="$output

## CI/CD ($ci_system)
"
            for cf in $ci_files; do
                output="$output
- \`$cf\`"
            done
        fi

        # Architecture notes from README
        if [ -n "$readme_file" ]; then
            output="$output

## Documentation

See \`$readme_file\` for project documentation."
        fi

        output="$output

---
Generated by loki onboard (depth $depth) on $(date +%Y-%m-%d)"
    fi

    # --- Output ---
    if [ "$use_stdout" = true ]; then
        echo "$output"
        return 0
    fi

    # Determine output path
    if [ -z "$output_path" ]; then
        output_path="$target_path/.claude/CLAUDE.md"
    fi

    # Handle update mode
    if [ "$update_mode" = true ] && [ -f "$output_path" ]; then
        local timestamp
        timestamp=$(date +%Y-%m-%d)
        local update_marker="## Updated: $timestamp"
        # Append new findings after a separator
        {
            cat "$output_path"
            echo ""
            echo "---"
            echo ""
            echo "$update_marker"
            echo ""
            echo "$output"
        } > "${output_path}.tmp"
        mv "${output_path}.tmp" "$output_path"
        log_info "Updated: $output_path"
        return 0
    fi

    # Create directory and write
    local output_dir
    output_dir=$(dirname "$output_path")
    mkdir -p "$output_dir"

    echo "$output" > "$output_path"
    log_info "Generated: $output_path"
    log_info "Project: $project_name | $src_count source files | $test_count tests | $doc_count docs"

    if [ -n "$languages" ]; then
        log_info "Languages: $languages"
    fi
    if [ -n "$frameworks" ]; then
        log_info "Frameworks: $frameworks"
    fi
}


# Analyze any codebase and explain its architecture in plain English (v6.31.0)
cmd_explain() {
    local target_path="."
    set +e
    local output_json=false
    local output_brief=false
    local output_save=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --json) output_json=true; require_jq || return 1; shift ;;
            --brief) output_brief=true; shift ;;
            --save) output_save=true; shift ;;
            --help|-h)
                echo -e "${BOLD}loki explain${NC} - Analyze any codebase and explain its architecture"
                echo ""
                echo "Usage: loki explain [path] [options]"
                echo ""
                echo "Arguments:"
                echo "  path              Path to repository (default: current directory)"
                echo ""
                echo "Options:"
                echo "  --json            Machine-readable JSON output"
                echo "  --brief           Condensed one-pager version"
                echo "  --save            Write output to EXPLAIN.md in the analyzed directory"
                echo "  --help            Show this help"
                echo ""
                echo "Examples:"
                echo "  loki explain                       # Analyze current directory"
                echo "  loki explain ~/projects/myapp      # Analyze specific repo"
                echo "  loki explain --brief               # Condensed overview"
                echo "  loki explain --json                # JSON output for tooling"
                echo "  loki explain --save                # Save as EXPLAIN.md"
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                echo "Run 'loki explain --help' for usage."
                return 1
                ;;
            *) target_path="$1"; shift ;;
        esac
    done

    if [ ! -d "$target_path" ]; then
        log_error "Directory not found: $target_path"
        return 1
    fi

    target_path="$(cd "$target_path" && pwd)"
    local project_name
    project_name="$(basename "$target_path")"

    local languages="" frameworks="" build_system="" test_framework=""
    local entry_points="" package_manager="" project_description=""
    local project_version="" detected_patterns="" scripts_info=""

    # --- package.json (JavaScript/TypeScript) ---
    if [ -f "$target_path/package.json" ]; then
        languages="$languages JavaScript/TypeScript"
        package_manager="npm"
        [ -f "$target_path/yarn.lock" ] && package_manager="yarn"
        [ -f "$target_path/pnpm-lock.yaml" ] && package_manager="pnpm"
        [ -f "$target_path/bun.lockb" ] && package_manager="bun"

        if command -v python3 &>/dev/null; then
            local pkg_meta
            # WAVE8 loki-est: pass the path via env (os.environ) instead of
            # interpolating into the python source -- a repo path containing an
            # apostrophe broke the string literal and dropped all package.json
            # metadata.
            pkg_meta=$(LOKI_EXP_PKG="$target_path/package.json" python3 -c "
import json, os
try:
    d = json.load(open(os.environ['LOKI_EXP_PKG']))
    print(d.get('name', ''))
    print(d.get('description', ''))
    print(d.get('version', ''))
    main = d.get('main', d.get('module', ''))
    print(main)
    s = d.get('scripts', {})
    for k in sorted(s.keys()):
        print(f'script:{k}:{s[k]}')
    for dep in list(d.get('dependencies', {}).keys()):
        print(f'dep:{dep}')
    for dep in list(d.get('devDependencies', {}).keys()):
        print(f'devdep:{dep}')
except: pass
" 2>/dev/null || true)
            local pkg_name pkg_desc pkg_ver pkg_main
            pkg_name=$(echo "$pkg_meta" | sed -n '1p')
            pkg_desc=$(echo "$pkg_meta" | sed -n '2p')
            pkg_ver=$(echo "$pkg_meta" | sed -n '3p')
            pkg_main=$(echo "$pkg_meta" | sed -n '4p')
            [ -n "$pkg_name" ] && project_name="$pkg_name"
            [ -n "$pkg_desc" ] && project_description="$pkg_desc"
            [ -n "$pkg_ver" ] && project_version="$pkg_ver"
            [ -n "$pkg_main" ] && entry_points="$pkg_main"
            scripts_info=$(echo "$pkg_meta" | grep '^script:' | sed 's/^script://' || true)
            local deps_list devdeps_list all_deps
            deps_list=$(echo "$pkg_meta" | grep '^dep:' | sed 's/^dep://' || true)
            devdeps_list=$(echo "$pkg_meta" | grep '^devdep:' | sed 's/^devdep://' || true)
            all_deps="$deps_list
$devdeps_list"
            echo "$deps_list" | grep -q '^react$' && frameworks="$frameworks React" || true
            echo "$deps_list" | grep -q '^next$' && frameworks="$frameworks Next.js" || true
            echo "$deps_list" | grep -q '^vue$' && frameworks="$frameworks Vue" || true
            echo "$deps_list" | grep -q '^express$' && frameworks="$frameworks Express" || true
            echo "$deps_list" | grep -q '^fastify$' && frameworks="$frameworks Fastify" || true
            echo "$deps_list" | grep -q '^svelte$' && frameworks="$frameworks Svelte" || true
            echo "$deps_list" | grep -q '^@angular/core$' && frameworks="$frameworks Angular" || true
            echo "$deps_list" | grep -q '^hono$' && frameworks="$frameworks Hono" || true
            echo "$deps_list" | grep -q '^astro$' && frameworks="$frameworks Astro" || true
            echo "$deps_list" | grep -q '^nuxt$' && frameworks="$frameworks Nuxt" || true
            echo "$deps_list" | grep -q '^remix$' && frameworks="$frameworks Remix" || true
            echo "$deps_list" | grep -q '^electron$' && frameworks="$frameworks Electron" || true
            echo "$all_deps" | grep -q '^tailwindcss$' && frameworks="$frameworks Tailwind" || true
            echo "$all_deps" | grep -q '^jest$' && test_framework="$test_framework jest" || true
            echo "$all_deps" | grep -q '^vitest$' && test_framework="$test_framework vitest" || true
            echo "$all_deps" | grep -q '^mocha$' && test_framework="$test_framework mocha" || true
            echo "$all_deps" | grep -q '^@playwright/test$' && test_framework="$test_framework playwright" || true
            echo "$all_deps" | grep -q '^cypress$' && test_framework="$test_framework cypress" || true
            echo "$all_deps" | grep -q '^webpack$' && build_system="webpack" || true
            echo "$all_deps" | grep -q '^vite$' && build_system="vite" || true
            echo "$all_deps" | grep -q '^esbuild$' && build_system="esbuild" || true
            echo "$all_deps" | grep -q '^rollup$' && build_system="rollup" || true
            echo "$all_deps" | grep -q '^turbo$' && build_system="turborepo" || true
            echo "$all_deps" | grep -q '^prisma$' && detected_patterns="$detected_patterns ORM(Prisma)" || true
            echo "$all_deps" | grep -q '^drizzle-orm$' && detected_patterns="$detected_patterns ORM(Drizzle)" || true
            echo "$all_deps" | grep -q '^@trpc/server$' && detected_patterns="$detected_patterns tRPC" || true
            echo "$all_deps" | grep -q '^graphql$' && detected_patterns="$detected_patterns GraphQL" || true
            echo "$all_deps" | grep -q '^socket.io$' && detected_patterns="$detected_patterns WebSocket" || true
            echo "$all_deps" | grep -q '^ws$' && detected_patterns="$detected_patterns WebSocket" || true
            echo "$all_deps" | grep -q '^@nestjs/core$' && frameworks="$frameworks NestJS" || true
            echo "$all_deps" | grep -q '^redis$\|^ioredis$' && detected_patterns="$detected_patterns Redis" || true
            echo "$all_deps" | grep -q '^mongoose$' && detected_patterns="$detected_patterns MongoDB" || true
            echo "$all_deps" | grep -q '^pg$' && detected_patterns="$detected_patterns PostgreSQL" || true
            echo "$all_deps" | grep -q '^stripe$' && detected_patterns="$detected_patterns Stripe" || true
        fi
    fi

    # --- Python ---
    if [ -f "$target_path/pyproject.toml" ]; then
        languages="$languages Python"
        package_manager="pip"
        grep -q '\[tool.poetry' "$target_path/pyproject.toml" 2>/dev/null && package_manager="poetry" || true
        grep -q "django" "$target_path/pyproject.toml" 2>/dev/null && frameworks="$frameworks Django" || true
        grep -q "flask" "$target_path/pyproject.toml" 2>/dev/null && frameworks="$frameworks Flask" || true
        grep -q "fastapi" "$target_path/pyproject.toml" 2>/dev/null && frameworks="$frameworks FastAPI" || true
        grep -q "sqlalchemy" "$target_path/pyproject.toml" 2>/dev/null && detected_patterns="$detected_patterns ORM(SQLAlchemy)" || true
        grep -q "pytest" "$target_path/pyproject.toml" 2>/dev/null && test_framework="$test_framework pytest" || true
        [ -z "$project_description" ] && project_description=$(grep '^description' "$target_path/pyproject.toml" 2>/dev/null | head -1 | sed 's/^description *= *"//;s/"$//' || true)
        [ -z "$project_version" ] && project_version=$(grep '^version' "$target_path/pyproject.toml" 2>/dev/null | head -1 | sed 's/^version *= *"//;s/"$//' || true)
    elif [ -f "$target_path/requirements.txt" ] || [ -f "$target_path/setup.py" ]; then
        languages="$languages Python"
        [ -z "$package_manager" ] && package_manager="pip"
    fi

    # --- Go ---
    if [ -f "$target_path/go.mod" ]; then
        languages="$languages Go"
        package_manager="go-modules"; build_system="go"; test_framework="$test_framework go-test"
        grep -q "gin-gonic" "$target_path/go.mod" 2>/dev/null && frameworks="$frameworks Gin" || true
        grep -q "grpc" "$target_path/go.mod" 2>/dev/null && detected_patterns="$detected_patterns gRPC" || true
    fi

    # --- Rust ---
    if [ -f "$target_path/Cargo.toml" ]; then
        languages="$languages Rust"
        package_manager="cargo"; build_system="cargo"; test_framework="$test_framework cargo-test"
        grep -q "actix" "$target_path/Cargo.toml" 2>/dev/null && frameworks="$frameworks Actix" || true
        grep -q "axum" "$target_path/Cargo.toml" 2>/dev/null && frameworks="$frameworks Axum" || true
        grep -q "tokio" "$target_path/Cargo.toml" 2>/dev/null && detected_patterns="$detected_patterns Async(Tokio)" || true
    fi

    # --- Ruby ---
    if [ -f "$target_path/Gemfile" ]; then
        languages="$languages Ruby"; package_manager="bundler"
        grep -q "rails" "$target_path/Gemfile" 2>/dev/null && frameworks="$frameworks Rails" || true
        grep -q "rspec" "$target_path/Gemfile" 2>/dev/null && test_framework="$test_framework rspec" || true
    fi

    # --- Java/Kotlin ---
    if [ -f "$target_path/pom.xml" ]; then languages="$languages Java" && build_system="maven" && package_manager="maven"; fi
    if [ -f "$target_path/build.gradle" ] || [ -f "$target_path/build.gradle.kts" ]; then languages="$languages Java/Kotlin"; build_system="gradle"; fi

    # --- C/C++ ---
    if [ -f "$target_path/CMakeLists.txt" ]; then languages="$languages C/C++" && build_system="cmake"; fi
    if [ -f "$target_path/Makefile" ]; then [ -z "$build_system" ] && build_system="make"; fi

    # --- Shell ---
    local shell_count
    shell_count=$(find "$target_path" -maxdepth 2 -name "*.sh" -type f 2>/dev/null | wc -l | tr -d ' ')
    [ "$shell_count" -gt 3 ] && languages="$languages Bash"

    # --- Docker / CI ---
    local has_docker=false
    if [ -f "$target_path/Dockerfile" ]; then has_docker=true && detected_patterns="$detected_patterns Docker"; fi
    [ -f "$target_path/docker-compose.yml" ] || [ -f "$target_path/docker-compose.yaml" ] && detected_patterns="$detected_patterns DockerCompose"

    local ci_system=""
    [ -d "$target_path/.github/workflows" ] && ci_system="GitHub Actions"
    if [ -f "$target_path/.gitlab-ci.yml" ]; then ci_system="$ci_system GitLab CI"; fi

    # --- Monorepo ---
    local is_monorepo=false
    if [ -f "$target_path/lerna.json" ]; then is_monorepo=true && detected_patterns="$detected_patterns Monorepo(Lerna)"; fi
    if [ -f "$target_path/turbo.json" ]; then is_monorepo=true && detected_patterns="$detected_patterns Monorepo(Turborepo)"; fi
    if [ -f "$target_path/nx.json" ]; then is_monorepo=true && detected_patterns="$detected_patterns Monorepo(Nx)"; fi
    if [ -f "$target_path/pnpm-workspace.yaml" ]; then is_monorepo=true && detected_patterns="$detected_patterns Monorepo(pnpm)"; fi
    if [ -f "$target_path/package.json" ] && grep -q '"workspaces"' "$target_path/package.json" 2>/dev/null; then
        is_monorepo=true
        echo "$detected_patterns" | grep -q "Monorepo" || detected_patterns="$detected_patterns Monorepo(npm)"
    fi

    # Deduplicate
    languages=$(echo "$languages" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')
    frameworks=$(echo "$frameworks" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')
    test_framework=$(echo "$test_framework" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')
    detected_patterns=$(echo "$detected_patterns" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')

    # --- README ---
    local readme_content="" readme_file=""
    for f in README.md readme.md README.rst README README.txt; do
        if [ -f "$target_path/$f" ]; then
            readme_file="$f"
            readme_content=$(head -80 "$target_path/$f" 2>/dev/null || true)
            break
        fi
    done
    if [ -z "$project_description" ] && [ -n "$readme_content" ]; then
        project_description=$(echo "$readme_content" | grep -v '^#' | grep -v '^$' | grep -v '^\[' | grep -v '^!' | grep -v '^---' | head -3 | sed 's/^ *//' | tr '\n' ' ' | sed 's/  */ /g;s/^ *//;s/ *$//')
    fi

    # --- File counts ---
    # Same SIGPIPE pipeline as cmd_onboard, but a DIFFERENT symptom here: this
    # assigns into a bare `tree_output=` after the local declaration, so under
    # -e a 141 aborts. Where a site instead writes `local x=$(...)`, `local`
    # resets $? and the 141 is swallowed -- silent truncation, no abort. Both
    # are wrong; `|| true` plus -e (worktree .git is a FILE) fixes both.
    local tree_output=""
    if command -v git &>/dev/null && [ -e "$target_path/.git" ]; then
        tree_output=$(cd "$target_path" && git ls-files 2>/dev/null | head -500 || true)
    else
        tree_output=$(find "$target_path" -maxdepth 4 -type f \
            -not -path '*/node_modules/*' -not -path '*/.git/*' \
            -not -path '*/vendor/*' -not -path '*/__pycache__/*' \
            -not -path '*/dist/*' -not -path '*/build/*' \
            -not -path '*/.next/*' -not -path '*/target/*' \
            2>/dev/null | sed "s|$target_path/||" | sort | head -500 || true)
    fi
    local total_files src_count test_count doc_count config_count
    total_files=$(echo "$tree_output" | { grep -c . || true; })
    src_count=$(echo "$tree_output" | { grep -cE '\.(js|ts|tsx|jsx|py|rs|go|rb|java|kt|c|cpp|h|hpp|sh|swift|cs|php)$' || true; })
    test_count=$(echo "$tree_output" | { grep -cE '(\.test\.|\.spec\.|_test\.|_spec\.|tests/|test/|__tests__/)' || true; })
    doc_count=$(echo "$tree_output" | { grep -cE '\.(md|rst|txt)$' || true; })
    config_count=$(echo "$tree_output" | { grep -cE '\.(json|yaml|yml|toml|cfg|ini|env)$|Dockerfile|Makefile' || true; })

    local top_dirs
    top_dirs=$(echo "$tree_output" | sed 's|/.*||' | sort | uniq -c | sort -rn | head -20)

    # Detect patterns from directory structure
    echo "$tree_output" | grep -qE '^(api|routes|controllers)/' && detected_patterns="$detected_patterns REST"
    echo "$tree_output" | grep -qE '^(pages|app)/' && detected_patterns="$detected_patterns FileRouting"
    echo "$tree_output" | grep -q '^components/' && detected_patterns="$detected_patterns ComponentBased" || true
    echo "$tree_output" | grep -qE '(middleware|middlewares)/' && detected_patterns="$detected_patterns Middleware"
    echo "$tree_output" | grep -qE '(events|listeners|handlers)/' && detected_patterns="$detected_patterns EventDriven"
    echo "$tree_output" | grep -qE '(models|entities|schemas)/' && detected_patterns="$detected_patterns MVC"
    echo "$tree_output" | grep -qE '(services|service)/' && detected_patterns="$detected_patterns ServiceLayer"
    detected_patterns=$(echo "$detected_patterns" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')

    # --- Detect commands ---
    local build_cmd="" run_cmd="" test_cmd="" lint_cmd=""
    if [ -n "$scripts_info" ]; then
        local has_build has_dev has_start has_test has_lint
        has_build=$(echo "$scripts_info" | grep '^build:' | head -1)
        has_dev=$(echo "$scripts_info" | grep '^dev:' | head -1)
        has_start=$(echo "$scripts_info" | grep '^start:' | head -1)
        has_test=$(echo "$scripts_info" | grep '^test:' | head -1)
        has_lint=$(echo "$scripts_info" | grep '^lint:' | head -1)
        [ -n "$has_build" ] && build_cmd="${package_manager:-npm} run build"
        [ -n "$has_dev" ] && run_cmd="${package_manager:-npm} run dev"
        [ -z "$run_cmd" ] && [ -n "$has_start" ] && run_cmd="${package_manager:-npm} start"
        [ -n "$has_test" ] && test_cmd="${package_manager:-npm} test"
        [ -n "$has_lint" ] && lint_cmd="${package_manager:-npm} run lint"
    fi
    if [ -f "$target_path/Cargo.toml" ]; then build_cmd="cargo build" && run_cmd="cargo run" && test_cmd="cargo test"; fi
    if [ -f "$target_path/go.mod" ]; then build_cmd="go build ./..." && run_cmd="go run ." && test_cmd="go test ./..."; fi
    if [ -f "$target_path/Makefile" ]; then
        [ -z "$build_cmd" ] && build_cmd="make"
        grep -q '^test:' "$target_path/Makefile" 2>/dev/null && [ -z "$test_cmd" ] && test_cmd="make test" || true
    fi
    if [ -f "$target_path/pyproject.toml" ]; then grep -q '\[tool.pytest' "$target_path/pyproject.toml" 2>/dev/null && [ -z "$test_cmd" ] && test_cmd="pytest" || true; fi

    # --- Key entry point files ---
    local major_files=""
    for candidate in \
        "src/index.ts" "src/index.js" "src/main.ts" "src/main.js" "src/app.ts" "src/app.js" \
        "index.ts" "index.js" "main.ts" "main.js" "app.ts" "app.js" "server.ts" "server.js" \
        "src/App.tsx" "src/App.jsx" "pages/index.tsx" "app/page.tsx" \
        "main.py" "app.py" "manage.py" "src/main.py" "__main__.py" \
        "main.go" "cmd/main.go" "src/main.rs" "src/lib.rs"; do
        echo "$tree_output" | grep -q "^${candidate}$" && major_files="$major_files $candidate"
    done
    major_files=$(echo "$major_files" | sed 's/^ *//')

    # --- JSON output ---
    if [ "$output_json" = true ]; then
        # WAVE8 loki-est: pass every value via the environment and read it with
        # os.environ instead of interpolating raw bash into the python source.
        # Interpolation broke on any apostrophe/quote/newline in a project name,
        # version, description, or path (e.g. a dir named `my'app`), silently
        # degrading real analysis to `{"error": "JSON generation failed"}`.
        # env-passing is injection-proof and keeps the same output shape.
        LOKI_EXP_NAME="$project_name" \
        LOKI_EXP_DESC="$project_description" \
        LOKI_EXP_VERSION="$project_version" \
        LOKI_EXP_PATH="$target_path" \
        LOKI_EXP_LANGUAGES="$languages" \
        LOKI_EXP_FRAMEWORKS="$frameworks" \
        LOKI_EXP_BUILD="$build_system" \
        LOKI_EXP_PKGMGR="$package_manager" \
        LOKI_EXP_TESTFW="$test_framework" \
        LOKI_EXP_CI="$ci_system" \
        LOKI_EXP_PATTERNS="$detected_patterns" \
        LOKI_EXP_TOTAL="$total_files" \
        LOKI_EXP_SRC="$src_count" \
        LOKI_EXP_TEST="$test_count" \
        LOKI_EXP_DOC="$doc_count" \
        LOKI_EXP_CONFIG="$config_count" \
        LOKI_EXP_BUILDCMD="$build_cmd" \
        LOKI_EXP_RUNCMD="$run_cmd" \
        LOKI_EXP_TESTCMD="$test_cmd" \
        LOKI_EXP_LINTCMD="$lint_cmd" \
        LOKI_EXP_ENTRY="$major_files" \
        LOKI_EXP_MONOREPO="$is_monorepo" \
        LOKI_EXP_DOCKER="$has_docker" \
        python3 -c "
import json, os

def _s(name):
    return os.environ.get(name, '')

def _list(name):
    v = _s(name).strip()
    return v.split() if v else []

def _opt(name):
    v = _s(name).strip()
    return v or None

def _int(name):
    try:
        return int(_s(name).strip())
    except (ValueError, TypeError):
        return 0

def _bool(name):
    return _s(name).strip() == 'true'

data = {
    'project': {'name': _s('LOKI_EXP_NAME'), 'description': _s('LOKI_EXP_DESC'), 'version': _s('LOKI_EXP_VERSION'), 'path': _s('LOKI_EXP_PATH')},
    'stack': {'languages': _list('LOKI_EXP_LANGUAGES'), 'frameworks': _list('LOKI_EXP_FRAMEWORKS'), 'build_system': _opt('LOKI_EXP_BUILD'), 'package_manager': _opt('LOKI_EXP_PKGMGR'), 'test_framework': _list('LOKI_EXP_TESTFW'), 'ci': _opt('LOKI_EXP_CI')},
    'patterns': _list('LOKI_EXP_PATTERNS'),
    'files': {'total': _int('LOKI_EXP_TOTAL'), 'source': _int('LOKI_EXP_SRC'), 'test': _int('LOKI_EXP_TEST'), 'docs': _int('LOKI_EXP_DOC'), 'config': _int('LOKI_EXP_CONFIG')},
    'commands': {'build': _opt('LOKI_EXP_BUILDCMD'), 'run': _opt('LOKI_EXP_RUNCMD'), 'test': _opt('LOKI_EXP_TESTCMD'), 'lint': _opt('LOKI_EXP_LINTCMD')},
    'entry_points': _list('LOKI_EXP_ENTRY'),
    'monorepo': _bool('LOKI_EXP_MONOREPO'),
    'has_docker': _bool('LOKI_EXP_DOCKER')
}
print(json.dumps(data, indent=2))
" 2>/dev/null || echo '{"error": "JSON generation failed"}'
        return 0
    fi

    # --- Text output ---
    local output=""
    local sep="================================================================================"
    local sep2="--------------------------------------------------------------------------------"

    if [ "$output_brief" = true ]; then
        output="${BOLD}$project_name${NC}"
        [ -n "$project_version" ] && output="$output (v$project_version)"
        output="$output
$sep2"
        [ -n "$project_description" ] && output="$output
$project_description
"
        output="$output
${BOLD}Stack:${NC} ${languages:-unknown}"
        [ -n "$frameworks" ] && output="$output + $frameworks"
        [ -n "$build_system" ] && output="$output | Build: $build_system"
        [ -n "$package_manager" ] && output="$output | Pkg: $package_manager"
        output="$output
${BOLD}Scope:${NC} $total_files files ($src_count source, $test_count test, $doc_count docs)"
        [ -n "$detected_patterns" ] && output="$output
${BOLD}Patterns:${NC} $(echo "$detected_patterns" | tr ' ' ', ')"
        [ -n "$ci_system" ] && output="$output
${BOLD}CI/CD:${NC} $(echo "$ci_system" | sed 's/^ *//')"
        [ -n "$test_framework" ] && output="$output
${BOLD}Testing:${NC} $test_framework"
        if [ -n "$run_cmd" ] || [ -n "$build_cmd" ] || [ -n "$test_cmd" ]; then
            output="$output
"
            [ -n "$run_cmd" ] && output="$output
${BOLD}Run:${NC}   $run_cmd"
            [ -n "$build_cmd" ] && output="$output
${BOLD}Build:${NC} $build_cmd"
            [ -n "$test_cmd" ] && output="$output
${BOLD}Test:${NC}  $test_cmd"
        fi
        [ -n "$major_files" ] && output="$output

${BOLD}Entry points:${NC} $(echo "$major_files" | tr ' ' ', ')"
        output="$output
$sep2
Generated by loki explain --brief on $(date +%Y-%m-%d)"
    else
        output="$sep
  PROJECT ANALYSIS: $project_name"
        [ -n "$project_version" ] && output="$output (v$project_version)"
        output="$output
$sep

${BOLD}EXECUTIVE SUMMARY${NC}
$sep2
"
        if [ -n "$project_description" ]; then
            output="$output$project_description"
        else
            output="${output}A ${languages:-software} project"
            [ -n "$frameworks" ] && output="$output built with $frameworks"
            [ -z "$readme_file" ] && output="$output. No README found -- description derived from project metadata."
            [ -n "$readme_file" ] && output="$output."
        fi
        output="$output

Scope: $total_files tracked files ($src_count source, $test_count test, $doc_count docs, $config_count config).

${BOLD}ARCHITECTURE OVERVIEW${NC}
$sep2
"
        if [ -n "$major_files" ]; then
            output="${output}Entry points:"
            for ef in $major_files; do
                output="$output
  - $ef"
            done
            output="$output
"
        fi
        output="${output}
Directory structure (by file count):"
        while IFS= read -r line; do
            [ -z "$line" ] && continue
            local dcount dname
            dcount=$(echo "$line" | awk '{print $1}')
            dname=$(echo "$line" | awk '{print $2}')
            if [ -d "$target_path/$dname" ]; then
                output="$output
  $dname/  ($dcount files)"
            else
                output="$output
  $dname"
            fi
        done <<< "$top_dirs"
        [ "$is_monorepo" = true ] && output="$output

This is a monorepo project."
        output="$output

${BOLD}TECHNOLOGY STACK${NC}
$sep2
Languages:       ${languages:-N/A}
Frameworks:      ${frameworks:-N/A}
Build system:    ${build_system:-N/A}
Package manager: ${package_manager:-N/A}
Test framework:  ${test_framework:-N/A}
CI/CD:           ${ci_system:-N/A}
"
        if [ -n "$detected_patterns" ]; then
            output="$output
${BOLD}KEY PATTERNS${NC}
$sep2"
            for pattern in $detected_patterns; do
                case "$pattern" in
                    REST) output="$output
  - REST API (route/controller directories detected)" ;;
                    GraphQL) output="$output
  - GraphQL API" ;;
                    gRPC) output="$output
  - gRPC (protocol buffer based RPC)" ;;
                    tRPC) output="$output
  - tRPC (end-to-end typesafe APIs)" ;;
                    WebSocket) output="$output
  - WebSocket (real-time communication)" ;;
                    EventDriven) output="$output
  - Event-driven architecture" ;;
                    MVC) output="$output
  - MVC pattern (model/entity directories detected)" ;;
                    ServiceLayer) output="$output
  - Service layer pattern" ;;
                    ComponentBased) output="$output
  - Component-based UI architecture" ;;
                    FileRouting) output="$output
  - File-based routing (pages/app directory)" ;;
                    Middleware) output="$output
  - Middleware pipeline" ;;
                    Docker) output="$output
  - Docker containerized" ;;
                    DockerCompose) output="$output
  - Docker Compose multi-service" ;;
                    Monorepo*) output="$output
  - $pattern" ;;
                    *) output="$output
  - $pattern" ;;
                esac
            done
            output="$output
"
        fi
        output="$output
${BOLD}GETTING STARTED${NC}
$sep2
"
        if [ -n "$package_manager" ]; then
            case "$package_manager" in
                npm) output="${output}Install dependencies:  npm install
" ;;
                yarn) output="${output}Install dependencies:  yarn install
" ;;
                pnpm) output="${output}Install dependencies:  pnpm install
" ;;
                pip|poetry) output="${output}Install dependencies:  ${package_manager} install
" ;;
                cargo) output="${output}Build:  cargo build
" ;;
                go-modules) output="${output}Download dependencies:  go mod download
" ;;
                bundler) output="${output}Install dependencies:  bundle install
" ;;
                *) output="${output}Package manager: $package_manager
" ;;
            esac
        fi
        [ -n "$run_cmd" ] && output="${output}
Run (development):  $run_cmd
"
        [ -n "$build_cmd" ] && output="${output}
Build:  $build_cmd
"
        [ -n "$test_cmd" ] && output="${output}
Test:  $test_cmd
"
        [ -n "$lint_cmd" ] && output="${output}
Lint:  $lint_cmd
"
        output="$output
${BOLD}FOR NEW CONTRIBUTORS${NC}
$sep2
"
        if [ -n "$readme_file" ]; then
            output="${output}Start by reading $readme_file for project-specific context.
"
        else
            output="${output}Note: This project has no README. Consider adding one.
"
        fi
        if [ -n "$major_files" ]; then
            output="${output}
Key files to understand first:"
            for ef in $major_files; do
                output="$output
  - $ef"
            done
            output="$output
"
        fi
        [ "$test_count" -gt 0 ] && [ -n "$test_cmd" ] && output="${output}
Tests: $test_count test files found. Run '$test_cmd' to verify your changes.
"
        output="$output
$sep
Generated by loki explain on $(date +%Y-%m-%d)"
    fi

    # --- Output ---
    if [ "$output_save" = true ]; then
        local clean_output
        clean_output=$(echo -e "$output" | sed 's/\x1b\[[0-9;]*m//g')
        echo "$clean_output" > "$target_path/EXPLAIN.md"
        echo -e "${GREEN}Saved to $target_path/EXPLAIN.md${NC}"
    else
        echo -e "$output"
    fi
    set -euo pipefail
}

# Generate, update, check, and manage project documentation (v6.75.0)
cmd_docs() {
    local subcmd="${1:-}"
    shift 2>/dev/null || true

    case "$subcmd" in
        generate) _docs_generate "$@" ;;
        update)   _docs_update "$@" ;;
        check)    _docs_check "$@" ;;
        status)   _docs_status "$@" ;;
        --help|-h|help|"")
            echo -e "${BOLD}loki docs${NC} - Generate, update, check project documentation"
            echo ""
            echo "Usage: loki docs <command> [options]"
            echo ""
            echo "Commands:"
            echo "  generate [path]    Generate full documentation suite in .loki/docs/"
            echo "  update [path]      Incremental update based on git changes since last generation"
            echo "  check [path]       Validate documentation coverage and staleness (exit 0=pass, 1=fail)"
            echo "  status [path]      Show documentation metrics (count, coverage, staleness)"
            echo ""
            echo "Options:"
            echo "  --help, -h         Show this help"
            echo ""
            echo "Generated documentation:"
            echo "  .loki/docs/README.md         Project overview, setup, usage"
            echo "  .loki/docs/ARCHITECTURE.md   System design, data flow, key decisions"
            echo "  .loki/docs/API.md            Public API reference (endpoints, functions, classes)"
            echo "  .loki/docs/SETUP.md          Dev environment setup, dependencies, running locally"
            echo "  .loki/docs/COMPONENTS.md     Per-component/module documentation"
            echo "  .loki/docs/TESTING.md        Test strategy, running tests, coverage"
            echo "  .loki/docs/DECISIONS.md      Architectural Decision Records"
            echo "  .loki/docs/CLAUDE.md         AI agent context file"
            echo "  .loki/docs/docs-manifest.json  Manifest with SHAs and timestamps"
            echo ""
            echo "Examples:"
            echo "  loki docs generate                 # Generate docs for current project"
            echo "  loki docs generate ~/projects/app  # Generate docs for specific project"
            echo "  loki docs update                   # Update only changed components"
            echo "  loki docs check                    # Validate docs (CI-friendly)"
            echo "  loki docs status                   # Show doc metrics"
            return 0
            ;;
        *)
            log_error "Unknown docs command: $subcmd"
            echo "Run 'loki docs --help' for usage."
            return 1
            ;;
    esac
}

# --- Docs helper: scan project and build context ---
_docs_scan_project() {
    local target_path="$1"

    # Build file tree (excluding common noise).
    # Same SIGPIPE pipeline and same fix as cmd_onboard; see the comment there.
    local tree_output=""
    if command -v git &>/dev/null && [ -e "$target_path/.git" ]; then
        tree_output=$(cd "$target_path" && git ls-files 2>/dev/null | head -500 || true)
    else
        tree_output=$(find "$target_path" -maxdepth 4 -type f \
            -not -path '*/node_modules/*' -not -path '*/.git/*' \
            -not -path '*/vendor/*' -not -path '*/__pycache__/*' \
            -not -path '*/dist/*' -not -path '*/build/*' \
            -not -path '*/.next/*' -not -path '*/target/*' \
            2>/dev/null | sed "s|$target_path/||" | sort | head -500 || true)
    fi
    echo "$tree_output"
}

_docs_build_context() {
    local target_path="$1"
    local tree_output="$2"
    local context=""

    context="PROJECT PATH: $target_path
PROJECT NAME: $(basename "$target_path")
"

    # File tree summary
    local total_files src_count test_count
    total_files=$(echo "$tree_output" | { grep -c . || true; })
    src_count=$(echo "$tree_output" | { grep -cE '\.(js|ts|tsx|jsx|py|rs|go|rb|java|kt|c|cpp|h|hpp|sh|swift|cs|php)$' || true; })
    test_count=$(echo "$tree_output" | { grep -cE '(\.test\.|\.spec\.|_test\.|_spec\.|tests/|test/|__tests__/)' || true; })
    context="${context}
FILES: $total_files total, $src_count source, $test_count test
"

    # Top-level directories
    local top_dirs
    top_dirs=$(echo "$tree_output" | sed 's|/.*||' | sort | uniq -c | sort -rn | head -20)
    context="${context}
DIRECTORY STRUCTURE:
$top_dirs
"

    # File tree (first 200 files)
    context="${context}
FILE TREE:
$(echo "$tree_output" | head -200)
"

    # Key config files - read first 50 lines of each
    for cfg in package.json pyproject.toml Cargo.toml go.mod Gemfile pom.xml requirements.txt Makefile Dockerfile docker-compose.yml; do
        if [ -f "$target_path/$cfg" ]; then
            context="${context}
--- $cfg (first 50 lines) ---
$(head -50 "$target_path/$cfg" 2>/dev/null || true)
"
        fi
    done

    # README if present
    for f in README.md readme.md README.rst README; do
        if [ -f "$target_path/$f" ]; then
            context="${context}
--- $f (first 80 lines) ---
$(head -80 "$target_path/$f" 2>/dev/null || true)
"
            break
        fi
    done

    # Key entry point files (first 50 lines)
    local entry_count=0
    for candidate in \
        "src/index.ts" "src/index.js" "src/main.ts" "src/main.js" "src/app.ts" "src/app.js" \
        "index.ts" "index.js" "main.ts" "main.js" "app.ts" "app.js" "server.ts" "server.js" \
        "src/App.tsx" "src/App.jsx" "pages/index.tsx" "app/page.tsx" \
        "main.py" "app.py" "manage.py" "src/main.py" "__main__.py" \
        "main.go" "cmd/main.go" "src/main.rs" "src/lib.rs"; do
        if [ -f "$target_path/$candidate" ]; then
            context="${context}
--- $candidate (first 50 lines) ---
$(head -50 "$target_path/$candidate" 2>/dev/null || true)
"
            ((++entry_count))
            [ "$entry_count" -ge 5 ] && break
        fi
    done

    echo "$context"
}

# --- Docs helper: invoke AI provider and capture output ---
_docs_invoke_provider() {
    local prompt="$1"
    local provider="${LOKI_PROVIDER:-claude}"
    if [ -f ".loki/state/provider" ]; then
        provider=$(cat ".loki/state/provider" 2>/dev/null)
    fi

    local result=""
    local exit_code=0
    local timeout_cmd="timeout"
    command -v gtimeout &>/dev/null && timeout_cmd="gtimeout"
    command -v $timeout_cmd &>/dev/null || timeout_cmd=""
    local t_prefix=""
    [ -n "$timeout_cmd" ] && t_prefix="$timeout_cmd 120"

    case "$provider" in
        claude)
            # caveman HARD-SUPPRESS (captured deliverable): the claude stdout IS
            # the generated doc -- it is captured here and written verbatim to a
            # markdown file by both callers (loki:23869, loki:24239). A globally
            # active caveman would compress this stdout, corrupting the generated
            # CLAUDE.md / DECISIONS.md / README into caveman-speak. So disable
            # caveman unconditionally on this site (mirrors run.sh:3286, the
            # structurally identical captured `claude -p` resolution subcall).
            # env-prefix form because of the optional $t_prefix timeout wrapper.
            # No-op when caveman is absent.
            # Model pin (v9.25.0). This site makes EIGHT sequential provider
            # calls per doc-gen run, and without --model every one of them runs
            # on whatever the account default is -- Opus for many users, at 5x
            # Sonnet's input price and 5x its output price. Documentation is
            # summarization over a context this code already assembled; it is
            # not the workload that needs the top tier. Pinned to the same
            # default the review subcall uses (run.sh:13762), and overridable
            # for anyone who wants a different trade.
            # S6 lean child: text-only role, so the lean flag set (from
            # claude-flags.sh, lazily sourced) drops user hooks, MCP and tools.
            # When the child ledger helper exists the call uses
            # --output-format json so usage is recorded, then unwraps .result.
            local _lean=() _lf _ledger=false
            if ! declare -F loki_lean_child_flags >/dev/null 2>&1 \
               && [ -f "${_LOKI_SCRIPT_DIR:-.}/lib/claude-flags.sh" ]; then
                # shellcheck source=autonomy/lib/claude-flags.sh
                . "${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" 2>/dev/null || true
            fi
            if declare -F loki_lean_child_flags >/dev/null 2>&1; then
                while IFS= read -r _lf; do _lean+=("$_lf"); done < <(loki_lean_child_flags docs 2>/dev/null)
            fi
            declare -F loki_child_ledger_append >/dev/null 2>&1 && _ledger=true
            local _fmt=()
            [ "$_ledger" = true ] && _fmt=(--output-format json)
            _loki_perm_argv_for "$PWD" >/dev/null 2>&1 || true
            result=$($t_prefix env "SHELL=$_LOKI_CLAUDE_SHELL" CAVEMAN_DEFAULT_MODE=off claude -p ${_lean[@]+"${_lean[@]}"} ${_fmt[@]+"${_fmt[@]}"} --model "${LOKI_DOCS_MODEL:-sonnet}" "$prompt" 2>/dev/null) || exit_code=$?
            if [ "$_ledger" = true ] && [ -n "$result" ]; then
                loki_child_ledger_append docs "$result" >/dev/null 2>&1 || true
                result=$(printf '%s' "$result" | python3 -c 'import json,sys
try:
    d = json.load(sys.stdin)
    print(d.get("result", ""))
except Exception:
    pass' 2>/dev/null)
            fi
            ;;
        codex)
            result=$($t_prefix codex exec --sandbox workspace-write "$prompt" 2>/dev/null) || exit_code=$?
            ;;
        cline)
            result=$($t_prefix cline -y "$prompt" 2>/dev/null) || exit_code=$?
            ;;
        aider)
            result=$($t_prefix aider --message "$prompt" --yes-always --no-auto-commits < /dev/null 2>/dev/null) || exit_code=$?
            ;;
        *)
            log_error "Unknown provider: $provider"
            return 1
            ;;
    esac

    if [ $exit_code -ne 0 ] && [ -z "$result" ]; then
        log_error "Provider '$provider' failed (exit $exit_code)"
        return 1
    fi

    echo "$result"
}

# --- Docs helper: write manifest ---
_docs_write_manifest() {
    local docs_dir="$1"
    shift
    # Remaining args are file names

    local git_sha=""
    git_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
    local generated_at
    generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)

    local files_json="{"
    local first=true
    for doc_file in "$@"; do
        local fname
        fname=$(basename "$doc_file")
        if [ -f "$docs_dir/$fname" ]; then
            local file_sha
            file_sha=$(shasum -a 256 "$docs_dir/$fname" 2>/dev/null | awk '{print $1}' || echo "unknown")
            if [ "$first" = true ]; then
                first=false
            else
                files_json="${files_json},"
            fi
            files_json="${files_json}
    \"$fname\": {\"sha256\": \"$file_sha\", \"generated_at\": \"$generated_at\"}"
        fi
    done
    files_json="${files_json}
  }"

    cat > "$docs_dir/docs-manifest.json" <<MANIFEST_EOF
{
  "generated_at": "$generated_at",
  "git_sha": "$git_sha",
  "files": $files_json
}
MANIFEST_EOF
}

# --- loki docs generate ---
_docs_generate() {
    local target_path="."
    local docs_requested=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo "Usage: loki docs generate [--docs] [path]"
                echo "Generate full documentation suite in .loki/docs/"
                echo "  --docs   Generate even when LOKI_DOCS_TIER is small (explicit request)"
                return 0
                ;;
            --docs)
                docs_requested=true
                shift
                ;;
            -*)
                log_error "Unknown option: $1"
                return 1
                ;;
            *)
                target_path="$1"
                shift
                ;;
        esac
    done

    if [ ! -d "$target_path" ]; then
        log_error "Directory not found: $target_path"
        return 1
    fi
    target_path="$(cd "$target_path" && pwd)"

    # a small-tier run (caller exports LOKI_DOCS_TIER) does not pay for the
    # eight-doc suite unless docs were asked for explicitly.
    case "${LOKI_DOCS_TIER:-}" in
        small|simple)
            if [ "$docs_requested" != true ]; then
                log_info "Small tier: skipping the 8-doc suite (pass --docs to force)"
                return 0
            fi
            ;;
    esac

    local docs_dir="$target_path/.loki/docs"
    mkdir -p "$docs_dir"

    log_info "Scanning project at: $target_path"

    # Scan project
    local tree_output
    tree_output=$(_docs_scan_project "$target_path")
    local context
    context=$(_docs_build_context "$target_path" "$tree_output")

    local project_name
    project_name="$(basename "$target_path")"

    # Check if provider CLI is available
    local provider="${LOKI_PROVIDER:-claude}"
    if [ -f "$target_path/.loki/state/provider" ]; then
        provider=$(cat "$target_path/.loki/state/provider" 2>/dev/null)
    fi

    local provider_available=true
    if ! command -v "$provider" &>/dev/null; then
        provider_available=false
        log_warn "Provider '$provider' not found. Generating template-based docs instead."
    fi

    # Doc types to generate
    local doc_types="README ARCHITECTURE API SETUP COMPONENTS TESTING DECISIONS CLAUDE"
    local generated_files=""

    for doc_type in $doc_types; do
        local doc_file="${doc_type}.md"
        local prompt=""

        echo -e "  ${CYAN}Generating${NC} $doc_file ..."

        if [ "$provider_available" = true ]; then
            # Build a focused prompt for each doc type.
            #
            # CONTEXT FIRST, AND THE ORDER IS LOAD-BEARING. All eight doc types
            # share the same $context (measured at 4,269 tokens on this repo).
            # With the per-type instruction first, the eight prompts share no
            # common prefix, so every call re-sends the whole context at full
            # input price: ~29,883 redundant input tokens per doc-gen run.
            # Leading with the identical context makes it one cache write plus
            # seven cache reads at 0.1x. Do not "tidy" the instruction back to
            # the top -- that silently restores the redundant billing.
            case "$doc_type" in
                README)
                    prompt="$context

Based on the project context above, generate a comprehensive README.md file. Include: project title, description, features, installation instructions, usage examples, and contribution guidelines. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
                ARCHITECTURE)
                    prompt="$context

Based on the project context above, generate an ARCHITECTURE.md file. Include: system overview, high-level design, component diagram (as text), data flow, directory structure explanation, key design decisions, and technology choices. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
                API)
                    prompt="$context

Based on the project context above, generate an API.md file. Document all public APIs: REST endpoints, exported functions, classes, and their signatures. Include parameter types, return types, and brief descriptions. If no API is detected, document the main public interfaces. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
                SETUP)
                    prompt="$context

Based on the project context above, generate a SETUP.md file. Include: prerequisites, installation steps, environment variables, database setup, running locally, running in Docker (if applicable), and common troubleshooting. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
                COMPONENTS)
                    prompt="$context

Based on the project context above, generate a COMPONENTS.md file. Document each major component/module/directory: its purpose, key files, public interface, and dependencies on other components. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
                TESTING)
                    prompt="$context

Based on the project context above, generate a TESTING.md file. Include: test strategy, test types (unit, integration, e2e), how to run tests, test configuration, coverage goals, and CI integration. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
                DECISIONS)
                    prompt="$context

Based on the project context above, generate a DECISIONS.md file with Architectural Decision Records (ADRs). Infer key decisions from the technology stack, directory structure, and configuration. Each ADR should have: title, status, context, decision, and consequences. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
                CLAUDE)
                    prompt="$context

Based on the project context above, generate a CLAUDE.md file for AI agent context. Include: project overview, key commands (build, test, run, lint), project structure with brief descriptions of each directory, coding conventions, important files, and any gotchas. This file helps AI coding assistants understand the project quickly. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
                    ;;
            esac

            local result=""
            result=$(_docs_invoke_provider "$prompt" 2>/dev/null) || true

            if [ -n "$result" ]; then
                echo "$result" > "$docs_dir/$doc_file"
                generated_files="$generated_files $doc_file"
            else
                # Fallback to template
                _docs_generate_template "$doc_type" "$target_path" "$context" > "$docs_dir/$doc_file"
                generated_files="$generated_files $doc_file"
            fi
        else
            # Template-based generation (no AI provider)
            _docs_generate_template "$doc_type" "$target_path" "$context" > "$docs_dir/$doc_file"
            generated_files="$generated_files $doc_file"
        fi

        echo -e "  ${GREEN}[done]${NC} $doc_file"
    done

    # Write manifest
    # shellcheck disable=SC2086
    _docs_write_manifest "$docs_dir" $generated_files

    echo ""
    log_info "Documentation generated in $docs_dir/"
    log_info "Files: $(echo "$generated_files" | wc -w | tr -d ' ') docs + manifest"
    echo -e "  Manifest: ${CYAN}$docs_dir/docs-manifest.json${NC}"
}

# --- Docs helper: generate template-based doc (fallback when no AI provider) ---
_docs_generate_template() {
    local doc_type="$1"
    local target_path="$2"
    local context="$3"
    local project_name
    project_name="$(basename "$target_path")"
    local timestamp
    timestamp=$(date +%Y-%m-%d)

    # Extract basic metadata from context
    local files_line
    files_line=$(echo "$context" | grep '^FILES:' | head -1 || echo "FILES: unknown")

    case "$doc_type" in
        README)
            cat <<EOF
# $project_name

## Overview

Project at \`$target_path\`.

$files_line

## Getting Started

See SETUP.md for installation and development instructions.

## Documentation

- [Architecture](ARCHITECTURE.md)
- [API Reference](API.md)
- [Setup Guide](SETUP.md)
- [Components](COMPONENTS.md)
- [Testing](TESTING.md)
- [Decisions](DECISIONS.md)

---
Generated by loki docs generate on $timestamp
EOF
            ;;
        ARCHITECTURE)
            cat <<EOF
# Architecture

## Overview

This document describes the architecture of $project_name.

## Directory Structure

\`\`\`
$(echo "$context" | sed -n '/^DIRECTORY STRUCTURE:/,/^$/p' | tail -n +2)
\`\`\`

## Key Components

See COMPONENTS.md for detailed component documentation.

---
Generated by loki docs generate on $timestamp
EOF
            ;;
        API)
            cat <<EOF
# API Reference

## Overview

Public API documentation for $project_name.

TODO: Document public endpoints, functions, and classes.

---
Generated by loki docs generate on $timestamp
EOF
            ;;
        SETUP)
            cat <<EOF
# Setup Guide

## Prerequisites

See the project configuration files for required dependencies.

## Installation

Clone the repository and install dependencies.

## Running Locally

Refer to the project's build and run scripts.

---
Generated by loki docs generate on $timestamp
EOF
            ;;
        COMPONENTS)
            cat <<EOF
# Components

## Overview

Component documentation for $project_name.

## Modules

$(echo "$context" | sed -n '/^DIRECTORY STRUCTURE:/,/^$/p' | tail -n +2 | awk '{print "### " $2 "\n\n" $1 " files\n"}')

---
Generated by loki docs generate on $timestamp
EOF
            ;;
        TESTING)
            cat <<EOF
# Testing

## Overview

Test documentation for $project_name.

## Running Tests

Refer to the project's test configuration for instructions.

---
Generated by loki docs generate on $timestamp
EOF
            ;;
        DECISIONS)
            cat <<EOF
# Architectural Decision Records

## Overview

Key architectural decisions for $project_name.

## ADR-001: [Title]

- **Status**: Proposed
- **Context**: [Why this decision was needed]
- **Decision**: [What was decided]
- **Consequences**: [What are the trade-offs]

---
Generated by loki docs generate on $timestamp
EOF
            ;;
        CLAUDE)
            cat <<EOF
# $project_name

## Project Overview

$files_line

## Project Structure

\`\`\`
$(echo "$context" | sed -n '/^FILE TREE:/,/^---/p' | head -50 | tail -n +2)
\`\`\`

## Key Commands

Refer to the project's configuration files for build, test, and run commands.

---
Generated by loki docs generate on $timestamp
EOF
            ;;
    esac
}

# --- loki docs update ---
_docs_update() {
    local target_path="."

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo "Usage: loki docs update [path]"
                echo "Incrementally update docs based on git changes since last generation."
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                return 1
                ;;
            *)
                target_path="$1"
                shift
                ;;
        esac
    done

    if [ ! -d "$target_path" ]; then
        log_error "Directory not found: $target_path"
        return 1
    fi
    target_path="$(cd "$target_path" && pwd)"

    local docs_dir="$target_path/.loki/docs"
    local manifest="$docs_dir/docs-manifest.json"

    if [ ! -f "$manifest" ]; then
        log_warn "No docs-manifest.json found. Running full generation instead."
        _docs_generate "$target_path"
        return $?
    fi

    # Get the SHA from last generation
    local last_sha=""
    if command -v python3 &>/dev/null; then
        last_sha=$(python3 -c "
import json
try:
    d = json.load(open('$manifest'))
    print(d.get('git_sha', ''))
except: pass
" 2>/dev/null || true)
    fi

    if [ -z "$last_sha" ] || [ "$last_sha" = "unknown" ]; then
        log_warn "Cannot determine last generation SHA. Running full generation."
        _docs_generate "$target_path"
        return $?
    fi

    # Check if HEAD is the same as last generation
    local current_sha
    current_sha=$(cd "$target_path" && git rev-parse HEAD 2>/dev/null || echo "unknown")

    if [ "$current_sha" = "$last_sha" ]; then
        log_info "Documentation is up to date (SHA: ${last_sha:0:8})"
        return 0
    fi

    # Get changed files since last generation
    local changed_files=""
    changed_files=$(cd "$target_path" && git diff --name-only "$last_sha"..HEAD 2>/dev/null || true)

    if [ -z "$changed_files" ]; then
        log_info "No file changes detected since last generation."
        return 0
    fi

    local changed_count
    changed_count=$(echo "$changed_files" | wc -l | tr -d ' ')
    log_info "Found $changed_count changed files since last generation (${last_sha:0:8}..${current_sha:0:8})"

    # Determine which doc types need updating based on changed files
    local docs_to_update=""

    # README: always update if any significant files changed
    if echo "$changed_files" | grep -qE '\.(md|txt|json|toml|yaml|yml)$'; then
        docs_to_update="$docs_to_update README"
    fi

    # ARCHITECTURE: update if directory structure or config changed
    if echo "$changed_files" | grep -qE '(package\.json|pyproject\.toml|Cargo\.toml|go\.mod|Dockerfile|docker-compose)'; then
        docs_to_update="$docs_to_update ARCHITECTURE"
    fi

    # API: update if source files changed
    if echo "$changed_files" | grep -qE '\.(ts|js|py|go|rs|java|rb)$'; then
        docs_to_update="$docs_to_update API COMPONENTS"
    fi

    # SETUP: update if config/dependency files changed
    if echo "$changed_files" | grep -qE '(package\.json|requirements\.txt|pyproject\.toml|Cargo\.toml|go\.mod|Dockerfile|\.env)'; then
        docs_to_update="$docs_to_update SETUP"
    fi

    # TESTING: update if test files or test config changed
    if echo "$changed_files" | grep -qE '(test|spec|\.test\.|\.spec\.|jest\.config|vitest\.config|pytest|\.bats)'; then
        docs_to_update="$docs_to_update TESTING"
    fi

    # CLAUDE: always update on any source change
    if echo "$changed_files" | grep -qE '\.(ts|js|py|go|rs|java|rb|sh)$'; then
        docs_to_update="$docs_to_update CLAUDE"
    fi

    # Deduplicate
    docs_to_update=$(echo "$docs_to_update" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')

    if [ -z "$docs_to_update" ]; then
        log_info "No documentation updates needed for the changed files."
        # Still update manifest SHA
        _docs_write_manifest "$docs_dir" $(ls "$docs_dir"/*.md 2>/dev/null | xargs -I{} basename {} || true)
        return 0
    fi

    log_info "Updating docs: $docs_to_update"

    # Re-scan and rebuild context
    local tree_output
    tree_output=$(_docs_scan_project "$target_path")
    local context
    context=$(_docs_build_context "$target_path" "$tree_output")

    # Add changed files context
    context="${context}
RECENTLY CHANGED FILES (since last doc generation):
$changed_files
"

    local provider="${LOKI_PROVIDER:-claude}"
    if [ -f "$target_path/.loki/state/provider" ]; then
        provider=$(cat "$target_path/.loki/state/provider" 2>/dev/null)
    fi

    local provider_available=true
    if ! command -v "$provider" &>/dev/null; then
        provider_available=false
        log_warn "Provider '$provider' not found. Generating template-based docs instead."
    fi

    local updated_files=""
    for doc_type in $docs_to_update; do
        local doc_file="${doc_type}.md"
        echo -e "  ${CYAN}Updating${NC} $doc_file ..."

        if [ "$provider_available" = true ]; then
            local existing_content=""
            [ -f "$docs_dir/$doc_file" ] && existing_content=$(cat "$docs_dir/$doc_file" 2>/dev/null || true)

            local prompt="Based on the following project context, update the ${doc_type}.md documentation. Here are the files that changed since the last generation:

$changed_files

Current ${doc_type}.md content:
$existing_content

Project context:
$context

Generate an updated ${doc_type}.md that reflects the current state of the project. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."

            local result=""
            result=$(_docs_invoke_provider "$prompt" 2>/dev/null) || true
            if [ -n "$result" ]; then
                echo "$result" > "$docs_dir/$doc_file"
            fi
        else
            _docs_generate_template "$doc_type" "$target_path" "$context" > "$docs_dir/$doc_file"
        fi

        updated_files="$updated_files $doc_file"
        echo -e "  ${GREEN}[done]${NC} $doc_file"
    done

    # Update manifest with all existing docs
    local all_doc_files=""
    for f in "$docs_dir"/*.md; do
        [ -f "$f" ] && all_doc_files="$all_doc_files $(basename "$f")"
    done
    # shellcheck disable=SC2086
    _docs_write_manifest "$docs_dir" $all_doc_files

    echo ""
    log_info "Updated $(echo "$updated_files" | wc -w | tr -d ' ') documentation files"
}

# --- loki docs check ---
_docs_check() {
    local target_path="."

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo "Usage: loki docs check [path]"
                echo "Validate documentation coverage and staleness. Exit 0=pass, 1=fail."
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                return 1
                ;;
            *)
                target_path="$1"
                shift
                ;;
        esac
    done

    if [ ! -d "$target_path" ]; then
        log_error "Directory not found: $target_path"
        return 1
    fi
    target_path="$(cd "$target_path" && pwd)"

    local docs_dir="$target_path/.loki/docs"
    local manifest="$docs_dir/docs-manifest.json"
    local pass=true

    echo -e "${BOLD}Documentation Check${NC}"
    echo "----------------"

    # Check 1: docs directory exists
    if [ ! -d "$docs_dir" ]; then
        echo -e "  ${RED}[FAIL]${NC} No .loki/docs/ directory found"
        echo ""
        echo "Run 'loki docs generate' to create documentation."
        return 1
    fi

    # Check 2: README.md exists and is non-empty
    if [ -f "$docs_dir/README.md" ] && [ -s "$docs_dir/README.md" ]; then
        echo -e "  ${GREEN}[PASS]${NC} README.md exists and is non-empty"
    else
        echo -e "  ${RED}[FAIL]${NC} README.md missing or empty"
        pass=false
    fi

    # Check 3: manifest exists
    if [ -f "$manifest" ]; then
        echo -e "  ${GREEN}[PASS]${NC} docs-manifest.json exists"
    else
        echo -e "  ${RED}[FAIL]${NC} docs-manifest.json missing"
        pass=false
    fi

    # Check 4: expected doc files exist
    local expected_docs="README.md ARCHITECTURE.md API.md SETUP.md COMPONENTS.md TESTING.md DECISIONS.md CLAUDE.md"
    local doc_count=0
    local expected_count=0
    for doc in $expected_docs; do
        ((++expected_count))
        if [ -f "$docs_dir/$doc" ] && [ -s "$docs_dir/$doc" ]; then
            ((++doc_count))
        else
            echo -e "  ${YELLOW}[WARN]${NC} Missing or empty: $doc"
        fi
    done
    local coverage_pct=0
    [ "$expected_count" -gt 0 ] && coverage_pct=$(( doc_count * 100 / expected_count ))
    echo -e "  ${BOLD}Coverage:${NC} $doc_count/$expected_count docs ($coverage_pct%)"

    # Check 5: staleness (commits since last doc generation)
    if [ -f "$manifest" ] && command -v python3 &>/dev/null; then
        local last_sha=""
        last_sha=$(python3 -c "
import json
try:
    d = json.load(open('$manifest'))
    print(d.get('git_sha', ''))
except: pass
" 2>/dev/null || true)

        if [ -n "$last_sha" ] && [ "$last_sha" != "unknown" ]; then
            local commits_behind=0
            commits_behind=$(cd "$target_path" && git rev-list --count "$last_sha"..HEAD 2>/dev/null || echo "0")
            if [ "$commits_behind" -gt 10 ]; then
                echo -e "  ${RED}[FAIL]${NC} Docs are $commits_behind commits behind HEAD (threshold: 10)"
                pass=false
            elif [ "$commits_behind" -gt 0 ]; then
                echo -e "  ${YELLOW}[WARN]${NC} Docs are $commits_behind commits behind HEAD"
            else
                echo -e "  ${GREEN}[PASS]${NC} Docs are up to date with HEAD"
            fi
        fi
    fi

    # Check 6: component coverage (check if major dirs have doc entries)
    local tree_output
    tree_output=$(_docs_scan_project "$target_path")
    local major_dirs
    major_dirs=$(echo "$tree_output" | sed 's|/.*||' | sort -u | head -20)
    local dir_count=0
    local documented_dirs=0
    if [ -f "$docs_dir/COMPONENTS.md" ]; then
        local components_content
        components_content=$(cat "$docs_dir/COMPONENTS.md" 2>/dev/null || true)
        while IFS= read -r dir; do
            [ -z "$dir" ] && continue
            [ ! -d "$target_path/$dir" ] && continue
            ((++dir_count))
            if echo "$components_content" | grep -qi "$dir" 2>/dev/null; then
                ((++documented_dirs))
            fi
        done <<< "$major_dirs"
        if [ "$dir_count" -gt 0 ]; then
            local dir_pct=$(( documented_dirs * 100 / dir_count ))
            echo -e "  ${BOLD}Component coverage:${NC} $documented_dirs/$dir_count directories ($dir_pct%)"
        fi
    fi

    echo ""
    if [ "$pass" = true ] && [ "$coverage_pct" -ge 75 ]; then
        echo -e "${GREEN}Result: PASS${NC}"
        return 0
    else
        echo -e "${RED}Result: FAIL${NC}"
        [ "$coverage_pct" -lt 75 ] && echo "  Doc coverage below 75%. Run 'loki docs generate' to create missing docs."
        return 1
    fi
}

# --- loki docs status ---
_docs_status() {
    local target_path="."

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo "Usage: loki docs status [path]"
                echo "Show documentation metrics."
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                return 1
                ;;
            *)
                target_path="$1"
                shift
                ;;
        esac
    done

    if [ ! -d "$target_path" ]; then
        log_error "Directory not found: $target_path"
        return 1
    fi
    target_path="$(cd "$target_path" && pwd)"

    local docs_dir="$target_path/.loki/docs"
    local manifest="$docs_dir/docs-manifest.json"

    echo -e "${BOLD}Documentation Status${NC}"
    echo "===================="
    echo ""

    if [ ! -d "$docs_dir" ]; then
        echo "No documentation generated yet."
        echo "Run 'loki docs generate' to create documentation."
        return 0
    fi

    # Doc count and sizes
    local doc_count=0
    local total_size=0
    echo -e "${BOLD}Documents:${NC}"
    for f in "$docs_dir"/*.md; do
        [ ! -f "$f" ] && continue
        ((++doc_count))
        local fsize
        fsize=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
        total_size=$((total_size + fsize))
        local flines
        flines=$(wc -l < "$f" 2>/dev/null | tr -d ' ')
        local fname
        fname=$(basename "$f")
        printf "  %-22s %5d lines  %6s\n" "$fname" "$flines" "$(_docs_human_size "$fsize")"
    done
    echo ""
    echo "  Total: $doc_count documents, $(_docs_human_size "$total_size")"
    echo ""

    # Coverage
    local tree_output
    tree_output=$(_docs_scan_project "$target_path")
    local total_files
    total_files=$(echo "$tree_output" | { grep -c . || true; })
    local src_files
    src_files=$(echo "$tree_output" | { grep -cE '\.(js|ts|tsx|jsx|py|rs|go|rb|java|kt|c|cpp|h|hpp|sh)$' || true; })

    local expected_docs="README.md ARCHITECTURE.md API.md SETUP.md COMPONENTS.md TESTING.md DECISIONS.md CLAUDE.md"
    local existing=0
    local expected_total=0
    for doc in $expected_docs; do
        ((++expected_total))
        [ -f "$docs_dir/$doc" ] && [ -s "$docs_dir/$doc" ] && ((++existing))
    done
    local pct=0
    [ "$expected_total" -gt 0 ] && pct=$(( existing * 100 / expected_total ))
    echo -e "${BOLD}Coverage:${NC} $existing/$expected_total doc types ($pct%)"
    echo "  Project: $total_files files ($src_files source)"
    echo ""

    # Staleness
    if [ -f "$manifest" ] && command -v python3 &>/dev/null; then
        local last_sha="" generated_at=""
        eval "$(python3 -c "
import json
try:
    d = json.load(open('$manifest'))
    print('last_sha=\"' + d.get('git_sha', 'unknown') + '\"')
    print('generated_at=\"' + d.get('generated_at', 'unknown') + '\"')
except: pass
" 2>/dev/null || true)"

        echo -e "${BOLD}Last generated:${NC} $generated_at"
        if [ -n "$last_sha" ] && [ "$last_sha" != "unknown" ]; then
            echo "  Git SHA: ${last_sha:0:12}"
            local commits_behind=0
            commits_behind=$(cd "$target_path" && git rev-list --count "$last_sha"..HEAD 2>/dev/null || echo "0")
            if [ "$commits_behind" -eq 0 ]; then
                echo -e "  Staleness: ${GREEN}up to date${NC}"
            elif [ "$commits_behind" -le 10 ]; then
                echo -e "  Staleness: ${YELLOW}$commits_behind commits behind${NC}"
            else
                echo -e "  Staleness: ${RED}$commits_behind commits behind${NC} (consider running 'loki docs update')"
            fi
        fi
    fi
}

# --- Docs helper: human-readable file size ---
_docs_human_size() {
    local bytes="$1"
    if [ "$bytes" -ge 1048576 ]; then
        echo "$((bytes / 1048576))MB"
    elif [ "$bytes" -ge 1024 ]; then
        echo "$((bytes / 1024))KB"
    else
        echo "${bytes}B"
    fi
}

#===============================================================================
# Magic: Spec-driven component generation (MagicModules + MoMoA)
#
# Storage layout:
#   .loki/magic/specs/<name>.md                - Canonical component specs
#   .loki/magic/generated/react/<name>.tsx     - React output
#   .loki/magic/generated/webcomponent/<name>.js - Web Component output
#   .loki/magic/generated/tests/<name>.test.tsx - Generated tests
#   .loki/magic/registry.json                  - Component registry
#===============================================================================

_magic_help() {
    echo -e "${BOLD}loki magic${NC} - Spec-driven component generation"
    echo ""
    echo "Usage: loki magic <command> [options]"
    echo ""
    echo "Commands:"
    echo "  generate <name>    Generate a component from a spec or screenshot"
    echo "  update             Regenerate components from their specs"
    echo "  list               List registered components"
    echo "  remove <name>      Remove a component and its artifacts"
    echo "  registry <op>      Registry operations (show|prune|stats)"
    echo "  debate <name>      Run multi-persona debate on an existing component"
    echo "  help               Show this help"
    echo ""
    echo "Options for 'generate':"
    echo "  --from-spec PATH           Generate from existing spec file"
    echo "  --from-screenshot PATH     Generate from a screenshot (vision-based)"
    echo "  --target TARGET            Output target: react|webcomponent|both (default: react)"
    echo "  --placement PATH           Where to place final component in project"
    echo ""
    echo "Options for 'update':"
    echo "  --name NAME                Update a specific component (default: all)"
    echo "  --force                    Overwrite local edits if any"
    echo ""
    echo "Options for 'list':"
    echo "  --format FMT               Output format: json|table (default: table)"
    echo ""
    echo "Options for 'remove':"
    echo "  --force                    Skip confirmation"
    echo ""
    echo "Options for 'debate':"
    echo "  --rounds N                 Number of debate rounds (default: 3)"
    echo ""
    echo "Examples:"
    echo "  loki magic generate LoginForm --from-spec specs/login.md"
    echo "  loki magic generate Navbar --from-screenshot shots/nav.png --target both"
    echo "  loki magic update --name LoginForm"
    echo "  loki magic list --format json"
    echo "  loki magic registry stats"
    echo "  loki magic debate LoginForm --rounds 5"
}

_magic_ensure_dirs() {
    mkdir -p ".loki/magic/specs" \
             ".loki/magic/generated/react" \
             ".loki/magic/generated/webcomponent" \
             ".loki/magic/generated/tests" 2>/dev/null || true
    if [ ! -f ".loki/magic/registry.json" ]; then
        # Schema matches magic/registry/schema.json: components as array
        echo '{"version": "1", "updated_at": "", "components": []}' > ".loki/magic/registry.json"
    fi
}

_magic_python() {
    # Prefer python3.12 if present (matches the rest of the project), fall back to python3
    if [ -x "/opt/homebrew/bin/python3.12" ]; then
        echo "/opt/homebrew/bin/python3.12"
    elif command -v python3.12 &>/dev/null; then
        command -v python3.12
    else
        echo "python3"
    fi
}

# Export PYTHONPATH so magic/* modules shipped with Loki are importable
# no matter which project directory the user runs from.
_magic_pypath() {
    local sk="${SKILL_DIR:-}"
    if [ -z "$sk" ]; then
        sk="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")"
    fi
    # Preserve any existing PYTHONPATH
    if [ -n "${PYTHONPATH:-}" ]; then
        echo "${sk}:${PYTHONPATH}"
    else
        echo "$sk"
    fi
}

_magic_valid_name() {
    local name="$1"
    [[ "$name" =~ ^[A-Za-z][A-Za-z0-9_-]*$ ]]
}

_magic_generate() {
    local name=""
    local from_spec=""
    local from_screenshot=""
    local target="react"
    local placement=""
    local description=""
    local tags=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --from-spec)
                from_spec="${2:-}"
                shift 2
                ;;
            --from-screenshot)
                from_screenshot="${2:-}"
                shift 2
                ;;
            --target)
                target="${2:-}"
                shift 2
                ;;
            --placement)
                placement="${2:-}"
                shift 2
                ;;
            --description|--desc)
                description="${2:-}"
                shift 2
                ;;
            --tags)
                tags="${2:-}"
                shift 2
                ;;
            --help|-h)
                _magic_help
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                return 1
                ;;
            *)
                if [ -z "$name" ]; then
                    name="$1"
                fi
                shift
                ;;
        esac
    done

    if [ -z "$name" ]; then
        log_error "Component name is required"
        echo "Usage: loki magic generate <name> [--from-spec PATH] [--from-screenshot PATH] [--target react|webcomponent|both] [--placement PATH]"
        return 1
    fi

    if ! _magic_valid_name "$name"; then
        log_error "Invalid component name: '$name' (must start with a letter, contain only letters, digits, _ or -)"
        return 1
    fi

    case "$target" in
        react|webcomponent|both) ;;
        *)
            log_error "Invalid target: '$target' (must be react, webcomponent, or both)"
            return 1
            ;;
    esac

    if [ -n "$from_spec" ] && [ ! -f "$from_spec" ]; then
        log_error "Spec file not found: $from_spec"
        return 1
    fi
    if [ -n "$from_screenshot" ] && [ ! -f "$from_screenshot" ]; then
        log_error "Screenshot file not found: $from_screenshot"
        return 1
    fi

    _magic_ensure_dirs
    local py
    py=$(_magic_python)

    log_info "Generating component '$name' (target: $target)"

    # 1. Build/load the canonical spec
    local spec_path=".loki/magic/specs/${name}.md"
    if [ -n "$from_spec" ]; then
        cp "$from_spec" "$spec_path"
        log_info "Copied spec from $from_spec"
    else
        log_info "Synthesizing spec via magic.core.spec"
        PYTHONPATH="$(_magic_pypath)" "$py" -c "
import sys
try:
    from magic.core.spec import generate_spec
except Exception as exc:
    sys.stderr.write(f'[ERROR] magic.core.spec not available: {exc}\n')
    sys.exit(2)
generate_spec(
    name='$name',
    out_path='$spec_path',
    from_spec='${from_spec}' or None,
    from_screenshot='${from_screenshot}' or None,
)
" || {
            log_error "Spec generation failed (module magic.core.spec missing or errored)"
            return 1
        }
    fi

    # 2. Load design tokens (optional; module may not yet exist)
    log_info "Loading design tokens"
    PYTHONPATH="$(_magic_pypath)" "$py" -c "
try:
    from magic.core.design_tokens import load_tokens
    load_tokens()
except Exception as exc:
    import sys
    sys.stderr.write(f'[WARN] design tokens unavailable: {exc}\n')
" 2>&1 | grep -v '^$' || true

    # 3. Generate component(s)
    log_info "Generating component via magic.core.generator"
    PYTHONPATH="$(_magic_pypath)" "$py" -c "
import sys
try:
    from magic.core.generator import generate_component
except Exception as exc:
    sys.stderr.write(f'[ERROR] magic.core.generator not available: {exc}\n')
    sys.exit(2)
generate_component(
    name='$name',
    spec_path='$spec_path',
    target='$target',
    react_out='.loki/magic/generated/react/${name}.tsx',
    wc_out='.loki/magic/generated/webcomponent/${name}.js',
    test_out='.loki/magic/generated/tests/${name}.test.tsx',
    placement='${placement}' or None,
)
" || {
        log_error "Component generation failed"
        return 1
    }

    # 4. Register in registry
    log_info "Registering component"
    # BUG-PU-010: pass free-text description/tags via environment variables and
    # read them with os.environ in the python body, instead of interpolating raw
    # user input into the python -c source (a value containing triple-quotes,
    # backslashes, newlines, or $(...) would crash the script or inject code).
    # Mirrors the LOKI_MEM_QUERY pattern in the memory-search path.
    LOKI_MAGIC_DESC="$description" LOKI_MAGIC_TAGS="$tags" \
    PYTHONPATH="$(_magic_pypath)" "$py" -c "
import os, sys
try:
    from magic.core.registry import register_component
except Exception as exc:
    sys.stderr.write(f'[ERROR] magic.core.registry not available: {exc}\n')
    sys.exit(2)
register_component(
    registry_path='.loki/magic/registry.json',
    name='$name',
    spec_path='$spec_path',
    target='$target',
    react_path='.loki/magic/generated/react/${name}.tsx' if '$target' in ('react','both') else '',
    webcomponent_path='.loki/magic/generated/webcomponent/${name}.js' if '$target' in ('webcomponent','both') else '',
    test_path='.loki/magic/generated/tests/${name}.test.tsx',
    description=os.environ.get('LOKI_MAGIC_DESC', '').strip(),
    tags=[t.strip() for t in os.environ.get('LOKI_MAGIC_TAGS', '').split(',') if t.strip()],
    placement='${placement}' or None,
)
" || {
        log_warn "Registry update failed; component files are still present"
    }

    log_info "Component '$name' generated successfully"
    echo "  Spec:   $spec_path"
    case "$target" in
        react)        echo "  React:  .loki/magic/generated/react/${name}.tsx" ;;
        webcomponent) echo "  WC:     .loki/magic/generated/webcomponent/${name}.js" ;;
        both)
            echo "  React:  .loki/magic/generated/react/${name}.tsx"
            echo "  WC:     .loki/magic/generated/webcomponent/${name}.js"
            ;;
    esac
    echo "  Tests:  .loki/magic/generated/tests/${name}.test.tsx"
}

_magic_update() {
    local name=""
    local force="false"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --name)
                name="${2:-}"
                shift 2
                ;;
            --force)
                force="true"
                shift
                ;;
            --help|-h)
                _magic_help
                return 0
                ;;
            *)
                log_error "Unknown option: $1"
                return 1
                ;;
        esac
    done

    # BUG-PU-010: validate --name (defense in depth, mirroring _magic_generate)
    # so a malicious value cannot reach the python body, AND pass it via an
    # environment variable instead of interpolating it into the python -c source.
    if [ -n "$name" ] && ! _magic_valid_name "$name"; then
        log_error "Invalid component name: '$name' (must start with a letter, contain only letters, digits, _ or -)"
        return 1
    fi

    _magic_ensure_dirs
    local py
    py=$(_magic_python)

    log_info "Updating components from specs (name=${name:-<all>}, force=$force)"
    LOKI_MAGIC_NAME="$name" \
    PYTHONPATH="$(_magic_pypath)" "$py" -c "
import os, sys
try:
    from magic.core.generator import update_components
except Exception as exc:
    sys.stderr.write(f'[ERROR] magic.core.generator not available: {exc}\n')
    sys.exit(2)
update_components(
    registry_path='.loki/magic/registry.json',
    name=os.environ.get('LOKI_MAGIC_NAME', '') or None,
    force=$([ "$force" = "true" ] && echo True || echo False),
)
" || {
        log_error "Update failed"
        return 1
    }

    log_info "Update complete"
}

_magic_list() {
    local format="table"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --format)
                format="${2:-}"
                shift 2
                ;;
            --help|-h)
                _magic_help
                return 0
                ;;
            *)
                log_error "Unknown option: $1"
                return 1
                ;;
        esac
    done

    case "$format" in
        json|table) ;;
        *)
            log_error "Invalid format: '$format' (must be json or table)"
            return 1
            ;;
    esac

    _magic_ensure_dirs
    local py
    py=$(_magic_python)

    PYTHONPATH="$(_magic_pypath)" "$py" -c "
import json, sys
try:
    with open('.loki/magic/registry.json') as fh:
        data = json.load(fh)
except Exception as exc:
    sys.stderr.write(f'[ERROR] could not read registry: {exc}\n')
    sys.exit(1)

components_raw = data.get('components', [])
# Normalize: registry schema uses list of entries; legacy format was dict
if isinstance(components_raw, dict):
    components = [{'name': k, **(v or {})} for k, v in components_raw.items()]
else:
    components = components_raw

fmt = '$format'

if fmt == 'json':
    print(json.dumps(components, indent=2, sort_keys=True))
else:
    if not components:
        print('No components registered. Use: loki magic generate <name>')
        sys.exit(0)
    print(f'{\"NAME\":<24} {\"VERSION\":<10} {\"TARGETS\":<24} {\"TAGS\"}')
    print('-' * 100)
    for entry in sorted(components, key=lambda e: e.get('name', '')):
        name = entry.get('name', '-')
        version = entry.get('version', '-')
        tgts = entry.get('targets', [])
        targets = ','.join(tgts) if isinstance(tgts, list) else str(tgts)
        tags = entry.get('tags', [])
        tags_str = ','.join(tags) if isinstance(tags, list) else str(tags)
        print(f'{name:<24} {version:<10} {targets:<24} {tags_str}')
"
}

_magic_remove() {
    local name=""
    local force="false"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --force)
                force="true"
                shift
                ;;
            --help|-h)
                _magic_help
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                return 1
                ;;
            *)
                if [ -z "$name" ]; then
                    name="$1"
                fi
                shift
                ;;
        esac
    done

    if [ -z "$name" ]; then
        log_error "Component name is required"
        echo "Usage: loki magic remove <name> [--force]"
        return 1
    fi

    if ! _magic_valid_name "$name"; then
        log_error "Invalid component name: '$name'"
        return 1
    fi

    _magic_ensure_dirs

    if [ "$force" != "true" ]; then
        echo -n "Remove component '$name' and all its artifacts? [y/N] "
        local reply
        read -r reply || reply=""
        case "$reply" in
            y|Y|yes|YES) ;;
            *)
                log_info "Aborted"
                return 0
                ;;
        esac
    fi

    rm -f ".loki/magic/specs/${name}.md" \
          ".loki/magic/generated/react/${name}.tsx" \
          ".loki/magic/generated/webcomponent/${name}.js" \
          ".loki/magic/generated/tests/${name}.test.tsx" 2>/dev/null || true

    local py
    py=$(_magic_python)
    PYTHONPATH="$(_magic_pypath)" "$py" -c "
import json, sys
try:
    with open('.loki/magic/registry.json') as fh:
        data = json.load(fh)
except Exception:
    data = {'components': {}, 'version': 1}
components = data.setdefault('components', {})
if '$name' in components:
    del components['$name']
with open('.loki/magic/registry.json', 'w') as fh:
    json.dump(data, fh, indent=2, sort_keys=True)
" || log_warn "Could not update registry.json"

    log_info "Removed component '$name'"
}

_magic_registry() {
    local op="${1:-show}"
    shift 2>/dev/null || true

    case "$op" in
        --help|-h|help)
            _magic_help
            return 0
            ;;
    esac

    _magic_ensure_dirs
    local py
    py=$(_magic_python)

    case "$op" in
        show)
            PYTHONPATH="$(_magic_pypath)" "$py" -c "
import json
with open('.loki/magic/registry.json') as fh:
    data = json.load(fh)
print(json.dumps(data, indent=2, sort_keys=True))
"
            ;;
        prune)
            log_info "Pruning registry entries with missing artifacts"
            PYTHONPATH="$(_magic_pypath)" "$py" -c "
import json, os, sys
try:
    from magic.core.registry import prune_registry
    removed = prune_registry('.loki/magic/registry.json')
    print(f'Pruned {removed} stale entries')
except Exception as exc:
    # Fallback prune: drop entries whose spec file is missing
    with open('.loki/magic/registry.json') as fh:
        data = json.load(fh)
    comps = data.get('components', {})
    removed = 0
    for name in list(comps.keys()):
        spec = (comps[name] or {}).get('spec_path')
        if not spec or not os.path.isfile(spec):
            del comps[name]
            removed += 1
    with open('.loki/magic/registry.json', 'w') as fh:
        json.dump(data, fh, indent=2, sort_keys=True)
    print(f'Pruned {removed} stale entries (fallback)')
"
            ;;
        stats)
            PYTHONPATH="$(_magic_pypath)" "$py" -c "
import json, os
with open('.loki/magic/registry.json') as fh:
    data = json.load(fh)
raw = data.get('components', []) or []
# Accept both array (new) and dict (legacy) shapes
if isinstance(raw, dict):
    comps = [{'name': k, **(v or {})} for k, v in raw.items()]
else:
    comps = raw
total = len(comps)
targets = {}
missing = 0
for entry in comps:
    entry = entry or {}
    for t in entry.get('targets', [entry.get('target', 'unknown')]):
        targets[t] = targets.get(t, 0) + 1
    spec = entry.get('spec_path')
    if not spec or not os.path.isfile(spec):
        missing += 1
print(f'Components:        {total}')
print(f'Missing specs:     {missing}')
print('By target:')
for t in sorted(targets):
    print(f'  {t:<14} {targets[t]}')
"
            ;;
        *)
            log_error "Unknown registry op: $op"
            echo "Available: show, prune, stats"
            return 1
            ;;
    esac
}

_magic_debate() {
    local name=""
    local rounds="3"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --rounds)
                rounds="${2:-}"
                shift 2
                ;;
            --help|-h)
                _magic_help
                return 0
                ;;
            -*)
                log_error "Unknown option: $1"
                return 1
                ;;
            *)
                if [ -z "$name" ]; then
                    name="$1"
                fi
                shift
                ;;
        esac
    done

    if [ -z "$name" ]; then
        log_error "Component name is required"
        echo "Usage: loki magic debate <name> --rounds N"
        return 1
    fi

    if ! _magic_valid_name "$name"; then
        log_error "Invalid component name: '$name'"
        return 1
    fi

    if ! [[ "$rounds" =~ ^[0-9]+$ ]] || [ "$rounds" -lt 1 ]; then
        log_error "--rounds must be a positive integer"
        return 1
    fi

    _magic_ensure_dirs
    local spec_path=".loki/magic/specs/${name}.md"
    if [ ! -f "$spec_path" ]; then
        log_error "No spec found for '$name' at $spec_path"
        return 1
    fi

    local py
    py=$(_magic_python)

    log_info "Running debate on '$name' ($rounds rounds)"
    PYTHONPATH="$(_magic_pypath)" "$py" -c "
import sys
try:
    from magic.core.debate import run_debate
except Exception as exc:
    sys.stderr.write(f'[ERROR] magic.core.debate not available: {exc}\n')
    sys.exit(2)
import json, os
# run_debate takes component_path, not react_path/wc_path -- passing those
# raised TypeError on EVERY invocation, and the caller's '|| true' swallowed it
# so Gate 12 reported PASS on total failure. Pick whichever generated artifact
# exists; the debate critiques code against the spec, so with neither present it
# still runs on the spec alone.
_react = '.loki/magic/generated/react/${name}.tsx'
_wc = '.loki/magic/generated/webcomponent/${name}.js'
_component = _react if os.path.exists(_react) else (_wc if os.path.exists(_wc) else '')
_result = run_debate(
    name='$name',
    spec_path='$spec_path',
    component_path=_component,
    rounds=$rounds,
)
# The result must reach stdout: the gate greps it for a blocking severity, and
# a returned-but-never-printed dict is invisible to every caller.
print(json.dumps(_result, indent=2, default=str))
if _result.get('error'):
    sys.stderr.write('[ERROR] debate error: %s\n' % _result['error'])
    sys.exit(1)
" || {
        log_error "Debate failed"
        return 1
    }

    log_info "Debate complete"
}

# =============================================================================
# loki wiki -- auto-generated, cited per-project codebase wiki + Q&A (R5).
#
# Loki's answer to Devin DeepWiki: a persistent, queryable wiki built from the
# codebase, where every section cites the real source files it came from, plus
# a grounded `ask` that returns cited answers (file:line). Heavy work lives in
# the Python core (autonomy/lib/wiki-generator.py, wiki-ask.py, wiki_index.py);
# this is a thin dispatcher, mirroring how cmd_proof delegates to the proof
# generator. Generation is incremental (skips when the codebase is unchanged).
# =============================================================================
# loki outcomes canary: installed access to the complete local canary workflow.
#
# Keep this as a thin dispatcher. Each Python tool retains its own explicit
# opt-in gate, input validation, privacy contract, writes (where applicable),
# and exit codes. Resolving from the installed Loki root makes the workflow
# usable from any current directory without copying repository-relative paths.
cmd_canary() {
    local subcommand="${1:-}"
    local tool_name=""
    local tools_dir="${_LOKI_SCRIPT_DIR}/../tools"

    case "$subcommand" in
        plan)     tool_name="outcome-canary.py" ;;
        record)   tool_name="outcome-canary-observe.py" ;;
        evaluate) tool_name="outcome-canary-evaluate.py" ;;
        receipt)  tool_name="outcome-canary-receipt.py" ;;
        verify)   tool_name="outcome-canary-receipt-verify.py" ;;
        --help|-h|help|"")
            echo -e "${BOLD}loki outcomes canary${NC} - run the local outcome-routing canary workflow"
            echo ""
            echo "Usage: loki outcomes canary <command> [options]"
            echo ""
            echo "Commands:"
            echo "  plan       Plan a deterministic reversible canary assignment"
            echo "  record     Record one explicitly consented local observation"
            echo "  evaluate   Produce an aggregate PROMOTE, HOLD, or ROLLBACK verdict"
            echo "  receipt    Create one portable immutable decision receipt"
            echo "  verify     Independently verify a receipt against current evidence"
            echo ""
            echo "Every mutating or decision-producing step remains explicitly opt-in."
            echo "Run 'loki outcomes canary <command> --help' for its exact arguments."
            return 0
            ;;
        *)
            echo "loki outcomes canary: unknown command: $subcommand" >&2
            echo "Run 'loki outcomes canary --help' for usage." >&2
            return 64
            ;;
    esac

    shift
    local tool="${tools_dir}/${tool_name}"
    if [ ! -f "$tool" ]; then
        echo "loki outcomes canary: installed tool is unavailable: $tool_name" >&2
        return 2
    fi
    if [ "$subcommand" = "verify" ]; then
        # this is a verify path. -E stops PYTHONPATH (an empty component
        # would add the cwd as an absolute path) and a committed
        # sitecustomize.py from acting before the tool runs. python3 is
        # resolved to an absolute path first so an empty PATH component (or a
        # relative one) cannot let a committed python3 shim in the checkout
        # being verified answer in its place.
        local py3
        py3="$(loki_verify_tool_path python3)" || {
            echo "loki outcomes canary verify: python3 is unavailable" >&2
            return 2
        }
        "$py3" -E "$tool" "$@"
        return $?
    fi
    python3 "$tool" "$@"
}

# loki outcomes: what happened to the work AFTER the receipt was written.
#
# Every competing agent reports VOLUME. Factory AI's analytics expose files
# edited, lines modified, commits, PRs created, tokens and DAU -- and no defect
# rate, no revert rate, no change-failure rate; their own telemetry doc leaves
# outcome correlation to the customer. Devin's security page concedes the agent
# "can still experience hallucinations, introduce bugs into code" and points at
# your existing review. Both can prove the agent was BUSY. Neither shows it was
# RIGHT. This answers the other question, from local git history alone.
#
# It reports UNKNOWN far more often than it reports a number, and that is the
# feature. See autonomy/lib/outcome_ledger.py for the anchor gate and the
# measured evidence behind it.
cmd_outcomes() {
    if [ "${1:-}" = "canary" ]; then
        shift
        cmd_canary "$@"
        return $?
    fi
    local lib="${_LOKI_SCRIPT_DIR}/lib/outcome_ledger.py"
    if [ ! -f "$lib" ]; then
        echo "outcome ledger is not installed at $lib" >&2
        return 2
    fi
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}loki outcomes${NC} - did the work turn out to be RIGHT, not just that it happened"
            echo ""
            echo "Usage: loki outcomes [--json] [--run-id <id>]"
            echo "       loki outcomes canary <command> [options]"
            echo ""
            echo "Follows each Evidence Receipt past the moment it was written and"
            echo "reports, from local git only: was it reverted, did its lines survive,"
            echo "how much was reworked, and the change-failure rate across receipts."
            echo ""
            echo "A receipt is measured ONLY when sha algebra proves base..head is that"
            echo "change. Everything else reads UNKNOWN with a named reason -- never 0,"
            echo "never a pass. Read-only: it never writes to the repo it analyses."
            echo ""
            echo "'loki outcomes canary' runs the local plan, record, evaluate, receipt,"
            echo "and verify workflow; each step retains its explicit opt-in gate."
            return 0
            ;;
    esac
    LOKI_DIR="${LOKI_DIR:-.loki}" python3 "$lib" "$@"
}

# loki control -- D56 control plane (on by default). The live route is the Bun CLI
# (loki-ts/src/commands/control.ts); this is the LOKI_LEGACY_BASH / no-shim
# fallback. It keeps the off switch and help in bash and hands the work to Bun.
cmd_control() {
    local sub="${1:-}"
    case "$sub" in
        ""|-h|--help|help)
            echo "Usage: loki control <command> [options]   (on by default; LOKI_CONTROL=0 turns it off; requires bun)"
            echo ""
            echo "Commands:"
            echo "  serve [--port N] [--db PATH]   Run the control plane + UI on 127.0.0.1"
            echo "  backfill [DIR]                 Ship DIR/.loki/runs to the control plane"
            echo "  prune --repo OWNER/NAME | --before ISO_DATE [--dry-run] [--db PATH]"
            echo "                                 Delete matching runs, their events and orphaned sources"
            echo "  stop                           Stop the running control plane"
            echo "  status [--json]                Show whether the control plane is reachable (--json: one JSON object)"
            return 0
            ;;
    esac
    if [ "${LOKI_CONTROL:-}" = "0" ]; then
        echo "loki control is off (LOKI_CONTROL=0). Unset it to turn the Control Plane back on."
        return 0
    fi
    local cli="$SKILL_DIR/loki-ts/dist/loki.js"
    if ! command -v bun >/dev/null 2>&1; then
        echo "loki control requires bun (https://bun.sh)" >&2
        return 1
    fi
    [ -f "$cli" ] || cli="$SKILL_DIR/loki-ts/src/cli.ts"
    exec bun --no-env-file --config=/dev/null "$cli" control "$@"
}

cmd_wiki() {
    local subcmd="${1:-}"
    shift 2>/dev/null || true

    local lib_dir="${_LOKI_SCRIPT_DIR}/lib"

    case "$subcmd" in
        generate) _wiki_generate "$lib_dir" "$@" ;;
        show)     _wiki_show "$@" ;;
        ask)      _wiki_ask "$lib_dir" "$@" ;;
        --help|-h|help|"")
            echo -e "${BOLD}loki wiki${NC} - Auto-generated, cited codebase wiki + Q&A"
            echo ""
            echo "Usage: loki wiki <command> [options]"
            echo ""
            echo "Commands:"
            echo "  generate [path] [--force]   Build/refresh the cited wiki in .loki/wiki/"
            echo "  show [section]              Print the wiki (or one section: architecture|modules|data-flow)"
            echo "  ask \"<question>\"           Cited answer grounded in the codebase (file:line)"
            echo ""
            echo "Each wiki section cites the real source files it was built from."
            echo "Generation is incremental: it skips when the codebase is unchanged."
            echo ""
            echo "Examples:"
            echo "  loki wiki generate                     # build wiki for current project"
            echo "  loki wiki show architecture            # show one section"
            echo "  loki wiki ask \"how does the cli dispatch commands\""
            return 0
            ;;
        *)
            log_error "Unknown wiki command: $subcmd"
            echo "Run 'loki wiki --help' for usage."
            return 1
            ;;
    esac
}

_wiki_generate() {
    local lib_dir="$1"; shift
    if ! command -v python3 >/dev/null 2>&1; then
        log_error "python3 is required for 'loki wiki generate'"
        return 1
    fi
    python3 "$lib_dir/wiki-generator.py" "$@"
}

_wiki_show() {
    local section=""
    local target="."
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo "Usage: loki wiki show [section]"
                echo "Sections: architecture, modules, data-flow"
                return 0
                ;;
            -*) log_error "Unknown option: $1"; return 1 ;;
            *) section="$1"; shift ;;
        esac
    done
    local wiki_dir="$target/.loki/wiki"
    if [ ! -d "$wiki_dir" ]; then
        log_error "No wiki found. Run 'loki wiki generate' first."
        return 1
    fi
    if [ -n "$section" ]; then
        local f="$wiki_dir/${section}.md"
        if [ ! -f "$f" ]; then
            log_error "No such section: $section (try: architecture, modules, data-flow)"
            return 1
        fi
        cat "$f"
    else
        if [ -f "$wiki_dir/index.md" ]; then
            cat "$wiki_dir/index.md"
        else
            log_error "Wiki index not found. Run 'loki wiki generate'."
            return 1
        fi
    fi
}

_wiki_ask() {
    local lib_dir="$1"; shift
    local question=""
    local extra=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo "Usage: loki wiki ask \"<question>\" [--json] [--k N]"
                return 0
                ;;
            --json|--quiet) extra+=("$1"); shift ;;
            --k) extra+=("--k" "${2:-6}"); shift 2 ;;
            *) if [ -z "$question" ]; then question="$1"; else question="$question $1"; fi; shift ;;
        esac
    done
    if [ -z "$question" ]; then
        log_error "Provide a question: loki wiki ask \"how does X work\""
        return 1
    fi
    if ! command -v python3 >/dev/null 2>&1; then
        log_error "python3 is required for 'loki wiki ask'"
        return 1
    fi
    python3 "$lib_dir/wiki-ask.py" --question "$question" "${extra[@]}"
}

cmd_magic() {
    local subcmd="${1:-help}"
    shift 2>/dev/null || true

    case "$subcmd" in
        generate|gen)    _magic_generate "$@" ;;
        update|up)       _magic_update "$@" ;;
        list|ls)         _magic_list "$@" ;;
        remove|rm)       _magic_remove "$@" ;;
        registry|reg)    _magic_registry "$@" ;;
        debate)          _magic_debate "$@" ;;
        help|--help|-h)  _magic_help ;;
        *)
            log_error "Unknown magic subcommand: $subcmd"
            _magic_help
            return 1
            ;;
    esac
}

# CI/CD quality gate integration (v6.22.0)
cmd_ci() {
    local ci_pr=false
    local ci_test_suggest=false
    local ci_report=false
    local ci_github_comment=false
    local ci_fail_on=""
    local ci_format="markdown"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki ci${NC} - CI/CD quality gate integration"
                echo ""
                echo "Usage: loki ci [options]"
                echo ""
                echo "Runs Loki Mode quality gates as a CI step. Works with GitHub Actions,"
                echo "GitLab CI, Jenkins, CircleCI, and other CI systems."
                echo ""
                echo "Modes:"
                echo "  --pr                 Review the current PR diff with all quality gates"
                echo "  --test-suggest       Generate test suggestions for changed files"
                echo "  --report             Generate a quality report"
                echo ""
                echo "Options:"
                echo "  --github-comment     Post review results as PR comment (needs GITHUB_TOKEN)"
                echo "  --fail-on <levels>   Set exit code 1 on severity: critical,high,medium,low"
                echo "  --format <fmt>       Output format: json, markdown, github (default: markdown)"
                echo "  --json               Alias for --format json"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Exit codes:"
                echo "  0  All checks passed (or below --fail-on threshold)"
                echo "  1  Findings exceed --fail-on threshold"
                echo "  2  Error (missing tools, invalid arguments)"
                echo ""
                echo "Environment variables (auto-detected):"
                echo "  GITHUB_ACTIONS       Detected when running in GitHub Actions"
                echo "  GITLAB_CI            Detected when running in GitLab CI"
                echo "  JENKINS_URL          Detected when running in Jenkins"
                echo "  CIRCLECI             Detected when running in CircleCI"
                echo "  GITHUB_TOKEN         Required for --github-comment"
                echo "  GITHUB_EVENT_PATH    Auto-set in GitHub Actions for PR context"
                echo ""
                echo "Examples:"
                echo "  loki ci --pr --format json              # Review PR diff as JSON"
                echo "  loki ci --pr --fail-on critical,high    # Fail CI on critical/high findings"
                echo "  loki ci --pr --github-comment           # Post results as PR comment"
                echo "  loki ci --report --format markdown      # Generate quality report"
                echo "  loki ci --test-suggest                  # Suggest tests for changed files"
                echo ""
                echo "GitHub Actions example:"
                echo "  - uses: asklokesh/loki-mode-action@v1"
                echo "    with:"
                echo "      command: loki ci --pr --github-comment --fail-on critical"
                return 0
                ;;
            --pr) ci_pr=true; shift ;;
            --test-suggest) ci_test_suggest=true; shift ;;
            --report) ci_report=true; shift ;;
            --github-comment) ci_github_comment=true; shift ;;
            --fail-on)
                shift
                ci_fail_on="${1:-}"
                if [ -z "$ci_fail_on" ]; then
                    echo -e "${RED}Error: --fail-on requires severity levels (e.g., critical,high)${NC}"
                    return 2
                fi
                ci_fail_on="$(echo "$ci_fail_on" | tr '[:upper:]' '[:lower:]')"
                shift
                ;;
            # --json is an alias for --format json. `loki ci` could already
            # emit JSON, but only under a flag name no other command uses, so a
            # script author reaching for the repo-wide --json convention found
            # nothing and concluded the capability was missing. Same output,
            # one spelling everywhere.
            --json)
                ci_format="json"
                shift
                ;;
            --format)
                shift
                ci_format="${1:-markdown}"
                ci_format="$(echo "$ci_format" | tr '[:upper:]' '[:lower:]')"
                if [[ "$ci_format" != "json" && "$ci_format" != "markdown" && "$ci_format" != "github" ]]; then
                    echo -e "${RED}Error: --format must be json, markdown, or github${NC}"
                    return 2
                fi
                shift
                ;;
            -*) echo -e "${RED}Unknown option: $1${NC}"; return 2 ;;
            *) echo -e "${RED}Unknown argument: $1${NC}"; return 2 ;;
        esac
    done

    # If no mode specified, default to --pr --report
    if [ "$ci_pr" = false ] && [ "$ci_test_suggest" = false ] && [ "$ci_report" = false ]; then
        ci_pr=true
        ci_report=true
    fi

    # --- Detect CI environment ---
    local ci_env="local"
    local ci_pr_number=""
    local ci_base_branch="main"
    local ci_repo=""

    if [ -n "${GITHUB_ACTIONS:-}" ]; then
        ci_env="github"
        ci_repo="${GITHUB_REPOSITORY:-}"
        ci_base_branch="${GITHUB_BASE_REF:-main}"
        # Extract PR number from GITHUB_EVENT_PATH
        if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -f "${GITHUB_EVENT_PATH:-}" ]; then
            ci_pr_number=$(python3 -c "
import json, sys
try:
    with open('${GITHUB_EVENT_PATH}') as f:
        event = json.load(f)
    pr = event.get('pull_request', event.get('number', ''))
    if isinstance(pr, dict):
        print(pr.get('number', ''))
    else:
        print(pr)
except Exception:
    print('')
" 2>/dev/null || echo "")
        fi
        # Fallback: GITHUB_REF_NAME for PR refs like refs/pull/123/merge
        if [ -z "$ci_pr_number" ] && [[ "${GITHUB_REF:-}" == refs/pull/*/merge ]]; then
            ci_pr_number=$(echo "${GITHUB_REF}" | sed 's|refs/pull/\([0-9]*\)/merge|\1|')
        fi
    elif [ -n "${GITLAB_CI:-}" ]; then
        ci_env="gitlab"
        ci_pr_number="${CI_MERGE_REQUEST_IID:-}"
        ci_base_branch="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-main}"
        ci_repo="${CI_PROJECT_PATH:-}"
    elif [ -n "${JENKINS_URL:-}" ]; then
        ci_env="jenkins"
        ci_pr_number="${CHANGE_ID:-}"
        ci_base_branch="${CHANGE_TARGET:-main}"
    elif [ -n "${CIRCLECI:-}" ]; then
        ci_env="circleci"
        ci_pr_number="${CIRCLE_PULL_REQUEST##*/}"
        ci_repo="${CIRCLE_PROJECT_USERNAME:-}/${CIRCLE_PROJECT_REPONAME:-}"
    fi

    # --- Gather diff ---
    local diff_content=""
    local changed_files=""

    if [ "$ci_pr" = true ] || [ "$ci_test_suggest" = true ] || [ "$ci_report" = true ]; then
        if [ -n "$ci_pr_number" ] && command -v gh &>/dev/null && [ "$ci_env" = "github" ]; then
            diff_content=$(gh pr diff "$ci_pr_number" 2>/dev/null || echo "")
            changed_files=$(gh pr diff "$ci_pr_number" --name-only 2>/dev/null || echo "")
        fi

        # Fallback: git diff against base branch
        if [ -z "$diff_content" ]; then
            # Fetch base branch if in CI
            if [ "$ci_env" != "local" ]; then
                git fetch origin "$ci_base_branch" --depth=1 2>/dev/null || true
            fi
            diff_content=$(git diff "origin/${ci_base_branch}...HEAD" 2>/dev/null || git diff HEAD~1 2>/dev/null || git diff HEAD 2>/dev/null || echo "")
            changed_files=$(git diff --name-only "origin/${ci_base_branch}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 2>/dev/null || git diff --name-only HEAD 2>/dev/null || echo "")
        fi

        if [ -z "$diff_content" ]; then
            if [ "$ci_format" = "json" ]; then
                echo '{"status":"skip","message":"No changes to review","ci_environment":"'"$ci_env"'","pr_number":"'"$ci_pr_number"'","findings":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"total":0},"exit_code":0}'
            else
                echo -e "${GREEN}No changes to review.${NC}"
            fi
            return 0
        fi
    fi

    # --- Severity helpers ---
    _ci_sev_level() {
        case "$1" in
            CRITICAL) echo 5 ;;
            HIGH) echo 4 ;;
            MEDIUM) echo 3 ;;
            LOW) echo 2 ;;
            INFO) echo 1 ;;
            *) echo 0 ;;
        esac
    }

    # Parse --fail-on into a minimum severity threshold
    local fail_threshold=99  # Default: never fail
    if [ -n "$ci_fail_on" ]; then
        # Take the lowest severity from the comma-separated list
        IFS=',' read -ra fail_levels <<< "$ci_fail_on"
        for level in "${fail_levels[@]}"; do
            level=$(echo "$level" | tr -d ' ' | tr '[:lower:]' '[:upper:]')
            local level_num
            level_num=$(_ci_sev_level "$level")
            if [ "$level_num" -gt 0 ] && [ "$level_num" -lt "$fail_threshold" ]; then
                fail_threshold=$level_num
            fi
        done
    fi

    # --- Run quality gates (reuses cmd_review logic) ---
    local findings=()
    local has_critical=false
    local has_high=false

    _ci_add_finding() {
        local file="$1" line="$2" sev="$3" cat="$4" finding="$5" suggestion="$6"
        findings+=("${file}|${line}|${sev}|${cat}|${finding}|${suggestion}")
        [ "$sev" = "CRITICAL" ] && has_critical=true || true
        [ "$sev" = "HIGH" ] && has_high=true || true
    }

    # Gate 1: Static Analysis - shellcheck
    if command -v shellcheck &>/dev/null; then
        local shell_files_ci
        shell_files_ci=$(echo "$changed_files" | grep -E '\.(sh|bash)$' || true)
        if [ -n "$shell_files_ci" ]; then
            while IFS= read -r sf; do
                [ -z "$sf" ] && continue
                [ ! -f "$sf" ] && continue
                local sc_out
                sc_out=$(shellcheck -f gcc "$sf" 2>/dev/null || true)
                while IFS= read -r sc_line; do
                    [ -z "$sc_line" ] && continue
                    local sc_file sc_lineno sc_sev sc_msg
                    sc_file=$(echo "$sc_line" | cut -d: -f1)
                    sc_lineno=$(echo "$sc_line" | cut -d: -f2)
                    sc_sev=$(echo "$sc_line" | sed -n 's/.*: \(warning\|error\|note\|info\):.*/\1/p')
                    sc_msg=$(echo "$sc_line" | sed 's/.*: \(warning\|error\|note\|info\): //')
                    local mapped_sev="LOW"
                    case "$sc_sev" in
                        error) mapped_sev="HIGH" ;;
                        warning) mapped_sev="MEDIUM" ;;
                        *) mapped_sev="LOW" ;;
                    esac
                    _ci_add_finding "$sc_file" "$sc_lineno" "$mapped_sev" "static-analysis" "$sc_msg" "Fix shellcheck finding"
                done <<< "$sc_out"
            done <<< "$shell_files_ci"
        fi
    fi

    # Gate 1b: Static Analysis - eslint
    if command -v npx &>/dev/null; then
        local js_files_ci
        js_files_ci=$(echo "$changed_files" | grep -E '\.(js|ts|jsx|tsx)$' || true)
        if [ -n "$js_files_ci" ]; then
            while IFS= read -r jsf; do
                [ -z "$jsf" ] && continue
                [ ! -f "$jsf" ] && continue
                if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then
                    local eslint_out
                    eslint_out=$(npx eslint --format compact "$jsf" 2>/dev/null || true)
                    while IFS= read -r el; do
                        [ -z "$el" ] && continue
                        [[ "$el" == *"problem"* ]] && continue
                        local el_file el_line el_msg
                        el_file=$(echo "$el" | cut -d: -f1)
                        el_line=$(echo "$el" | cut -d: -f2)
                        el_msg=$(echo "$el" | sed 's/^[^)]*) //')
                        local el_sev="LOW"
                        [[ "$el" == *"Error"* ]] && el_sev="MEDIUM"
                        _ci_add_finding "$el_file" "$el_line" "$el_sev" "static-analysis" "$el_msg" "Fix eslint finding"
                    done <<< "$eslint_out"
                fi
            done <<< "$js_files_ci"
        fi
    fi

    # Gate 2: Security Scan
    # Deny filter for the secret patterns below. This scanner sees RAW DIFF
    # LINES with no file context, so unlike the other two copies of this matcher
    # (autonomy/lib/secret-scan.sh, verify.sh) it cannot skip test paths -- it
    # has to judge the VALUE. A credential whose value literally says "test",
    # "example", "xxx", or "not-called" is a fixture; real leaked keys are not
    # named after their own fakeness.
    #
    # Concretely, this is what reported 2 CRITICAL findings on the v8 PR:
    #   sk-ant-xxx                 (loki-ts/test/runner/oauth_dev.test.ts)
    #   sk-ant-test-not-called     (loki-ts/tests/runner/quality_gates.test.ts)
    # Both exist to prove the SDK path fails closed without a real key. Flagging
    # them trains people to ignore CRITICAL, which is how a real one gets missed.
    local _ci_secret_deny='(test|example|sample|fixture|dummy|fake|placeholder|redacted|changeme|your[-_]|xxx+|not[-_]called|\$\{|\$[A-Za-z_]|process\.env|os\.(environ|getenv)|<[^>]*>)'

    _ci_scan() {
        local pattern="$1" sev="$2" cat="$3" finding="$4" suggestion="$5"
        # Only scan added lines (starting with "+") to avoid flagging deleted code
        while IFS= read -r match; do
            [ -z "$match" ] && continue
            # grep -n emits "N:content"; take the number before the first
            # colon without forking echo+cut per line (matters at finding
            # counts in the thousands+).
            local ml="${match%%:*}"
            _ci_add_finding "diff" "$ml" "$sev" "$cat" "$finding" "$suggestion"
        done < <(printf '%s\n' "$diff_content" \
            | grep -n '^+' \
            | grep -E "$pattern" 2>/dev/null \
            | grep -vEi "$_ci_secret_deny" 2>/dev/null || true)
    }

    # Hardcoded secrets
    local ci_secret_patterns=(
        'API_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
        'SECRET_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
        'PASSWORD\s*[=:]\s*["\x27][^\x27"]{4,}'
        'PRIVATE_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
        'AWS_ACCESS_KEY_ID\s*[=:]\s*["\x27]AK[A-Z0-9]{18}'
        'ghp_[A-Za-z0-9]{36}'
        'sk-[A-Za-z0-9]{32,}'
        'Bearer\s+[A-Za-z0-9._-]{20,}'
    )
    for pattern in "${ci_secret_patterns[@]}"; do
        _ci_scan "$pattern" "CRITICAL" "security" \
            "Potential hardcoded secret detected" \
            "Use environment variables or a secrets manager"
    done

    # SQL injection
    _ci_scan '(SELECT|INSERT|UPDATE|DELETE|DROP)\s.*\+\s*(req\.|request\.|params\.|user)' \
        "HIGH" "security" \
        "Potential SQL injection: string concatenation in query" \
        "Use parameterized queries or prepared statements"

    # eval/exec
    _ci_scan '(^|\s)(eval|exec)\s*\(' \
        "HIGH" "security" \
        "Dangerous eval/exec usage detected" \
        "Avoid eval/exec with dynamic input"

    # Unsafe deserialization
    _ci_scan '(pickle\.loads?|yaml\.load\s*\()' \
        "HIGH" "security" \
        "Unsafe deserialization detected" \
        "Use yaml.safe_load or avoid pickle with untrusted data"

    # Disabled SSL
    _ci_scan '(verify\s*=\s*False|VERIFY_SSL\s*=\s*False|NODE_TLS_REJECT_UNAUTHORIZED.*0|rejectUnauthorized.*false)' \
        "HIGH" "security" \
        "SSL verification disabled" \
        "Enable SSL verification in production"

    # Gate 3: Anti-patterns
    _ci_scan 'console\.(log|debug)\(' "LOW" "anti-pattern" \
        "console.log/debug statement found" \
        "Remove debug logging or use a proper logger"

    _ci_scan 'except\s*:' "MEDIUM" "anti-pattern" \
        "Bare except clause" \
        "Catch specific exceptions"

    # Gate 4: TODO/FIXME markers in new code
    _ci_scan '^\+.*\b(TODO|FIXME|HACK|XXX):' "INFO" "style" \
        "TODO/FIXME marker in new code" \
        "Track in issue tracker"

    # --- Test suggestions ---
    local test_suggestions=""
    if [ "$ci_test_suggest" = true ] && [ -n "$changed_files" ]; then
        # Changed-file list goes through a temp file, read as sys.argv[1], not
        # through an exported env var: an exported string counts toward the
        # same execve() size ceiling as argv (plus, on Linux, its own 128 KiB
        # per-string cap), so a PR touching enough files can crash this
        # heredoc invocation with "Argument list too long" (exit 126) exactly
        # like an oversized argv element would.
        local _ci_changed_tmp
        _ci_changed_tmp=$(mktemp "${TMPDIR:-/tmp}/loki-ci-changed-XXXXXX")
        printf '%s' "$changed_files" >"$_ci_changed_tmp"
        test_suggestions=$(python3 - "$_ci_changed_tmp" << 'TEST_SUGGEST_PY'
import sys, os

changed = open(sys.argv[1]).read().strip().split("\n")
suggestions = []

for f in changed:
    f = f.strip()
    if not f:
        continue

    base = os.path.basename(f)
    name, ext = os.path.splitext(base)
    dirpath = os.path.dirname(f)

    # Skip test files themselves
    if "test" in name.lower() or "spec" in name.lower():
        continue
    # Skip non-code files
    if ext not in (".py", ".js", ".ts", ".jsx", ".tsx", ".sh", ".bash", ".go", ".rs", ".rb"):
        continue

    # Suggest test file paths based on language conventions
    if ext == ".py":
        test_path = os.path.join(dirpath, f"test_{name}.py")
        alt_path = os.path.join("tests", f"test_{name}.py")
        suggestions.append({"file": f, "test_file": test_path, "alt_test_file": alt_path,
                          "framework": "pytest", "hint": f"Add unit tests for {name} module"})
    elif ext in (".js", ".ts", ".jsx", ".tsx"):
        if ext == ".tsx":
            test_ext = ".test.tsx"
        elif ext == ".jsx":
            test_ext = ".test.jsx"
        elif ext == ".ts":
            test_ext = ".test.ts"
        elif ext == ".js":
            test_ext = ".test.js"
        else:
            test_ext = f".test{ext}"
        test_path = os.path.join(dirpath, f"{name}{test_ext}")
        suggestions.append({"file": f, "test_file": test_path,
                          "framework": "jest/vitest", "hint": f"Add tests for {name} component/module"})
    elif ext in (".sh", ".bash"):
        test_path = os.path.join("tests", f"test-{name}.sh")
        suggestions.append({"file": f, "test_file": test_path,
                          "framework": "bash", "hint": f"Add shell tests for {name}"})
    elif ext == ".go":
        test_path = os.path.join(dirpath, f"{name}_test.go")
        suggestions.append({"file": f, "test_file": test_path,
                          "framework": "go test", "hint": f"Add Go tests for {name}"})
    elif ext == ".rs":
        suggestions.append({"file": f, "test_file": f"{f} (mod tests)",
                          "framework": "cargo test", "hint": f"Add #[cfg(test)] mod tests in {name}"})
    elif ext == ".rb":
        test_path = os.path.join("spec", f"{name}_spec.rb")
        suggestions.append({"file": f, "test_file": test_path,
                          "framework": "rspec", "hint": f"Add RSpec tests for {name}"})

import json
print(json.dumps(suggestions))
TEST_SUGGEST_PY
        )
        rm -f "$_ci_changed_tmp"
    fi

    # --- Tally findings ---
    local count_critical=0 count_high=0 count_medium=0 count_low=0 count_info=0
    for f in "${findings[@]+"${findings[@]}"}"; do
        # "file|line|sev|cat|finding|suggestion" (see _ci_add_finding above).
        # No echo+cut fork per finding -- this loop runs once per finding and
        # dominated the runtime at the fixture's old ARG_MAX-scale sizes.
        local sev _tally_file _tally_line _tally_cat _tally_finding _tally_suggestion
        IFS='|' read -r _tally_file _tally_line sev _tally_cat _tally_finding _tally_suggestion <<< "$f"
        case "$sev" in
            CRITICAL) count_critical=$((count_critical + 1)) ;;
            HIGH) count_high=$((count_high + 1)) ;;
            MEDIUM) count_medium=$((count_medium + 1)) ;;
            LOW) count_low=$((count_low + 1)) ;;
            INFO) count_info=$((count_info + 1)) ;;
        esac
    done
    local count_total=${#findings[@]}

    # Determine exit code based on --fail-on threshold
    local exit_code=0
    if [ "$fail_threshold" -lt 99 ]; then
        for f in "${findings[@]+"${findings[@]}"}"; do
            local sev sev_num
            sev=$(echo "$f" | cut -d'|' -f3)
            sev_num=$(_ci_sev_level "$sev")
            if [ "$sev_num" -ge "$fail_threshold" ]; then
                exit_code=1
                break
            fi
        done
    fi

    # --- Build output ---
    local report_timestamp
    report_timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    local file_count
    file_count=$(echo "$changed_files" | grep -c '.' 2>/dev/null || echo 0)

    if [ "$ci_format" = "json" ]; then
        # JSON output via python for proper escaping.
        #
        # Findings and test suggestions go through temp files, read as
        # sys.argv[1]/[2], not through exported env vars: an exported string
        # counts toward the same execve() size ceiling as argv (plus, on
        # Linux, its own 128 KiB per-string cap), so a large enough diff --
        # meaning enough findings, not necessarily a byte-huge diff -- crashed
        # this exact heredoc with "Argument list too long" (exit 126) before
        # this fix (BACKLOG 25). META and COUNTS stay as env vars: both are a
        # handful of fixed-width fields, bounded regardless of diff size.
        local _ci_findings_tmp _ci_tests_tmp
        _ci_findings_tmp=$(mktemp "${TMPDIR:-/tmp}/loki-ci-findings-XXXXXX")
        _ci_tests_tmp=$(mktemp "${TMPDIR:-/tmp}/loki-ci-tests-XXXXXX")
        : >"$_ci_findings_tmp"
        for f in "${findings[@]+"${findings[@]}"}"; do
            printf '%s\n' "$f" >>"$_ci_findings_tmp"
        done
        printf '%s' "${test_suggestions:-[]}" >"$_ci_tests_tmp"
        export LOKI_CI_JSON_META="ci_env=${ci_env}|pr=${ci_pr_number}|ts=${report_timestamp}|files=${file_count}|exit=${exit_code}"
        export LOKI_CI_JSON_COUNTS="critical=${count_critical}|high=${count_high}|medium=${count_medium}|low=${count_low}|info=${count_info}|total=${count_total}"

        python3 - "$_ci_findings_tmp" "$_ci_tests_tmp" << 'CI_JSON_OUT'
import json, os, sys

findings_raw = open(sys.argv[1]).read().strip().split("\n")
meta_raw = os.environ.get("LOKI_CI_JSON_META", "")
counts_raw = os.environ.get("LOKI_CI_JSON_COUNTS", "")
tests_raw = open(sys.argv[2]).read().strip() or "[]"

meta = dict(item.split("=", 1) for item in meta_raw.split("|") if "=" in item)
counts = dict(item.split("=", 1) for item in counts_raw.split("|") if "=" in item)

findings = []
for line in findings_raw:
    if not line or "|" not in line:
        continue
    parts = line.split("|", 5)
    if len(parts) >= 6:
        findings.append({
            "file": parts[0],
            "line": int(parts[1]) if parts[1].isdigit() else 0,
            "severity": parts[2],
            "category": parts[3],
            "finding": parts[4],
            "suggestion": parts[5]
        })

try:
    tests = json.loads(tests_raw)
except Exception:
    tests = []

report = {
    "status": "fail" if int(meta.get("exit", "0")) > 0 else "pass",
    "ci_environment": meta.get("ci_env", "local"),
    "pr_number": meta.get("pr", ""),
    "timestamp": meta.get("ts", ""),
    "files_changed": int(meta.get("files", "0")),
    "findings": findings,
    "summary": {
        "critical": int(counts.get("critical", "0")),
        "high": int(counts.get("high", "0")),
        "medium": int(counts.get("medium", "0")),
        "low": int(counts.get("low", "0")),
        "info": int(counts.get("info", "0")),
        "total": int(counts.get("total", "0"))
    },
    "exit_code": int(meta.get("exit", "0"))
}
if tests:
    report["test_suggestions"] = tests

print(json.dumps(report, indent=2))
CI_JSON_OUT
        rm -f "$_ci_findings_tmp" "$_ci_tests_tmp"
        unset LOKI_CI_JSON_META LOKI_CI_JSON_COUNTS

    elif [ "$ci_format" = "github" ]; then
        # GitHub-flavored markdown for PR comments
        echo "## Loki CI Quality Report"
        echo ""
        echo "| Metric | Count |"
        echo "|--------|-------|"
        echo "| Files changed | $file_count |"
        echo "| Critical | $count_critical |"
        echo "| High | $count_high |"
        echo "| Medium | $count_medium |"
        echo "| Low | $count_low |"
        echo "| Info | $count_info |"
        echo "| **Total** | **$count_total** |"
        echo ""

        if [ "$count_total" -gt 0 ]; then
            echo "### Findings"
            echo ""
            for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
                local has_sev=false
                for f in "${findings[@]+"${findings[@]}"}"; do
                    local f_sev
                    f_sev=$(echo "$f" | cut -d'|' -f3)
                    [ "$f_sev" != "$sev_name" ] && continue
                    if [ "$has_sev" = false ]; then
                        echo "#### $sev_name"
                        echo ""
                        has_sev=true
                    fi
                    local f_file f_line f_cat f_finding f_suggestion
                    f_file=$(echo "$f" | cut -d'|' -f1)
                    f_line=$(echo "$f" | cut -d'|' -f2)
                    f_cat=$(echo "$f" | cut -d'|' -f4)
                    f_finding=$(echo "$f" | cut -d'|' -f5)
                    f_suggestion=$(echo "$f" | cut -d'|' -f6)
                    echo "- **\`$f_file:$f_line\`** [$f_cat] $f_finding"
                    echo "  - Suggestion: $f_suggestion"
                done
                [ "$has_sev" = true ] && echo ""
            done
        else
            echo "No findings. All quality gates passed."
        fi

        if [ -n "${test_suggestions:-}" ] && [ "$test_suggestions" != "[]" ]; then
            echo "### Test Suggestions"
            echo ""
            # Piped on stdin, not through an exported env var (BACKLOG 25):
            # an exported string shares argv's execve() size ceiling.
            printf '%s' "$test_suggestions" | python3 -c "
import json, sys
tests = json.loads(sys.stdin.read() or '[]')
for t in tests:
    print(f\"- **{t['file']}**: {t['hint']} ({t['framework']})\")
    print(f\"  - Suggested: \`{t['test_file']}\`\")
" 2>/dev/null || true
        fi

        echo ""
        if [ "$exit_code" -eq 0 ]; then
            echo "**Result: PASSED**"
        else
            echo "**Result: FAILED** (findings exceed threshold)"
        fi
        echo ""
        echo "_Generated by [Loki Mode](https://github.com/asklokesh/loki-mode) at $report_timestamp"

    else
        # Default: markdown (terminal-friendly)
        echo -e "${BOLD}Loki CI Quality Report${NC}"
        echo -e "Environment: ${CYAN}$ci_env${NC}"
        [ -n "$ci_pr_number" ] && echo -e "PR: ${CYAN}#$ci_pr_number${NC}"
        echo -e "Files changed: ${CYAN}$file_count${NC}"
        echo -e "Timestamp: ${DIM}$report_timestamp${NC}"
        echo "---"

        if [ "$count_total" -eq 0 ]; then
            echo -e "${GREEN}All quality gates passed. No findings.${NC}"
        else
            for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
                local printed_header=false
                for f in "${findings[@]+"${findings[@]}"}"; do
                    local f_sev
                    f_sev=$(echo "$f" | cut -d'|' -f3)
                    [ "$f_sev" != "$sev_name" ] && continue
                    if [ "$printed_header" = false ]; then
                        local sev_color="$NC"
                        case "$sev_name" in
                            CRITICAL) sev_color="$RED" ;;
                            HIGH) sev_color="$RED" ;;
                            MEDIUM) sev_color="$YELLOW" ;;
                            LOW) sev_color="$CYAN" ;;
                            INFO) sev_color="$DIM" ;;
                        esac
                        echo ""
                        echo -e "${sev_color}${BOLD}[$sev_name]${NC}"
                        printed_header=true
                    fi
                    local f_file f_line f_cat f_finding f_suggestion
                    f_file=$(echo "$f" | cut -d'|' -f1)
                    f_line=$(echo "$f" | cut -d'|' -f2)
                    f_cat=$(echo "$f" | cut -d'|' -f4)
                    f_finding=$(echo "$f" | cut -d'|' -f5)
                    f_suggestion=$(echo "$f" | cut -d'|' -f6)
                    echo -e "  ${DIM}$f_file:$f_line${NC} [$f_cat] $f_finding"
                    echo -e "    -> $f_suggestion"
                done
            done
        fi

        echo ""
        echo "---"
        echo -e "Summary: ${RED}$count_critical critical${NC}, ${RED}$count_high high${NC}, ${YELLOW}$count_medium medium${NC}, ${CYAN}$count_low low${NC}, ${DIM}$count_info info${NC} ($count_total total)"

        if [ "$ci_test_suggest" = true ] && [ -n "${test_suggestions:-}" ] && [ "$test_suggestions" != "[]" ]; then
            echo ""
            echo -e "${BOLD}Test Suggestions${NC}"
            # Piped on stdin, not through an exported env var (BACKLOG 25):
            # an exported string shares argv's execve() size ceiling.
            printf '%s' "$test_suggestions" | python3 -c "
import json, sys
tests = json.loads(sys.stdin.read() or '[]')
for t in tests:
    print(f\"  {t['file']} -> {t['test_file']} ({t['framework']})\")
    print(f\"    {t['hint']}\")
" 2>/dev/null || true
        fi

        if [ "$exit_code" -eq 0 ]; then
            echo -e "${GREEN}Result: PASSED${NC}"
        else
            echo -e "${RED}Result: FAILED (findings exceed threshold)${NC}"
        fi
    fi

    # --- Post GitHub comment ---
    if [ "$ci_github_comment" = true ]; then
        if [ -z "${GITHUB_TOKEN:-}" ]; then
            echo -e "${YELLOW}Warning: --github-comment requires GITHUB_TOKEN. Skipping comment.${NC}" >&2
        elif [ -z "$ci_pr_number" ]; then
            echo -e "${YELLOW}Warning: No PR number detected. Skipping comment.${NC}" >&2
        elif command -v gh &>/dev/null; then
            # Generate GitHub-format report for comment
            local comment_body
            comment_body=$("$0" ci --pr --format github 2>/dev/null || echo "Loki CI report generation failed.")
            # Body goes through stdin (--body-file -), never as an argv element:
            # a large diff produces a large findings list, and a report body
            # passed via --body overflows ARG_MAX (exit 126) instead of posting.
            printf '%s' "$comment_body" | gh pr comment "$ci_pr_number" --body-file - 2>/dev/null && \
                echo -e "${GREEN}Posted review comment to PR #$ci_pr_number${NC}" >&2 || \
                echo -e "${YELLOW}Warning: Failed to post PR comment${NC}" >&2
        else
            echo -e "${YELLOW}Warning: gh CLI required for --github-comment. Install: https://cli.github.com${NC}" >&2
        fi
    fi

    return "$exit_code"
}

# AI-powered test generation (v6.24.0)
cmd_test() {
    local test_file=""
    local test_dir=""
    local test_changed=false
    local test_coverage=80
    local test_format=""
    local test_output=""
    local test_dry_run=false
    local test_json=false
    local test_verbose=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki test${NC} - AI-powered test generation"
                echo ""
                echo "Usage: loki test [options] [file-or-dir]"
                echo ""
                echo "Analyzes source files and generates comprehensive test suites."
                echo "Detects language and framework automatically. Works without API keys."
                echo ""
                echo "Sources (default: git-changed files):"
                echo "  --file <path>        Test a specific file"
                echo "  --dir <path>         Test all files in a directory"
                echo "  --changed            Test only git-changed files (default if no source given)"
                echo "  <file-or-dir>        Positional: auto-detect file or directory"
                echo ""
                echo "Options:"
                echo "  --coverage <n>       Target coverage percentage (default: 80)"
                echo "  --format <fmt>       Test framework: auto, jest, pytest, vitest, mocha, go-test"
                echo "  --output <path>      Custom output directory for generated tests"
                echo "  --dry-run            Preview what tests would be generated without writing"
                echo "  --json               JSON output"
                echo "  --verbose            Detailed output"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Supported languages:"
                echo "  JavaScript/TypeScript  (.js, .ts, .jsx, .tsx)  -> jest, vitest, mocha"
                echo "  Python                 (.py)                   -> pytest"
                echo "  Go                     (.go)                   -> go-test"
                echo "  Rust                   (.rs)                   -> cargo-test"
                echo "  Java                   (.java)                 -> junit"
                echo "  Ruby                   (.rb)                   -> rspec"
                echo "  Shell/Bash             (.sh)                   -> bats"
                echo ""
                echo "Exit codes:"
                echo "  0  Tests generated successfully"
                echo "  1  No testable files found"
                echo "  2  Error (invalid arguments, missing paths)"
                echo ""
                echo "Examples:"
                echo "  loki test                           # Generate tests for git-changed files"
                echo "  loki test --file src/utils.ts       # Test a specific file"
                echo "  loki test --dir src/lib/            # Test all files in directory"
                echo "  loki test --changed --format jest   # Changed files, force Jest format"
                echo "  loki test --dry-run                 # Preview without writing"
                echo "  loki test --coverage 90             # Target 90% coverage"
                echo "  loki test --json                    # Machine-readable output"
                echo "  loki test src/utils.ts              # Positional file argument"
                return 0
                ;;
            --file)
                shift
                test_file="${1:-}"
                if [ -z "$test_file" ]; then
                    echo -e "${RED}Error: --file requires a path${NC}"
                    return 2
                fi
                shift
                ;;
            --dir)
                shift
                test_dir="${1:-}"
                if [ -z "$test_dir" ]; then
                    echo -e "${RED}Error: --dir requires a path${NC}"
                    return 2
                fi
                shift
                ;;
            --changed) test_changed=true; shift ;;
            --coverage)
                shift
                test_coverage="${1:-80}"
                if ! [[ "$test_coverage" =~ ^[0-9]+$ ]] || [ "$test_coverage" -lt 1 ] || [ "$test_coverage" -gt 100 ]; then
                    echo -e "${RED}Error: --coverage must be a number between 1 and 100${NC}"
                    return 2
                fi
                shift
                ;;
            --format)
                shift
                test_format="${1:-}"
                if [ -z "$test_format" ]; then
                    echo -e "${RED}Error: --format requires a framework name${NC}"
                    return 2
                fi
                test_format="$(echo "$test_format" | tr '[:upper:]' '[:lower:]')"
                case "$test_format" in
                    auto|jest|pytest|vitest|mocha|go-test|cargo-test|junit|rspec|bats) ;;
                    *) echo -e "${RED}Error: unsupported format '$test_format'. Use: auto, jest, pytest, vitest, mocha, go-test, cargo-test, junit, rspec, bats${NC}"; return 2 ;;
                esac
                shift
                ;;
            --output)
                shift
                test_output="${1:-}"
                if [ -z "$test_output" ]; then
                    echo -e "${RED}Error: --output requires a path${NC}"
                    return 2
                fi
                shift
                ;;
            --dry-run) test_dry_run=true; shift ;;
            --json) test_json=true; shift ;;
            --verbose) test_verbose=true; shift ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}"
                echo "Run 'loki test --help' for usage."
                return 2
                ;;
            *)
                # Positional argument: auto-detect file or directory
                if [ -f "$1" ]; then
                    test_file="$1"
                elif [ -d "$1" ]; then
                    test_dir="$1"
                else
                    echo -e "${RED}Error: '$1' is not a file or directory${NC}"
                    return 2
                fi
                shift
                ;;
        esac
    done

    # Default to --changed if no source specified
    if [ -z "$test_file" ] && [ -z "$test_dir" ] && [ "$test_changed" = false ]; then
        test_changed=true
    fi

    # --- Collect source files ---
    local -a source_files=()

    if [ -n "$test_file" ]; then
        if [ ! -f "$test_file" ]; then
            echo -e "${RED}Error: file not found: $test_file${NC}"
            return 2
        fi
        source_files+=("$test_file")
    elif [ -n "$test_dir" ]; then
        if [ ! -d "$test_dir" ]; then
            echo -e "${RED}Error: directory not found: $test_dir${NC}"
            return 2
        fi
        while IFS= read -r f; do
            source_files+=("$f")
        done < <(_test_find_source_files "$test_dir")
    elif [ "$test_changed" = true ]; then
        while IFS= read -r f; do
            [ -n "$f" ] && [ -f "$f" ] && source_files+=("$f")
        done < <(git diff --name-only HEAD 2>/dev/null; git diff --name-only --cached 2>/dev/null; git diff --name-only 2>/dev/null)
        # Deduplicate
        if [ ${#source_files[@]} -gt 0 ]; then
            local -a unique_files=()
            local seen_list=""
            for f in "${source_files[@]}"; do
                if [[ "$seen_list" != *"|$f|"* ]]; then
                    seen_list="${seen_list}|$f|"
                    unique_files+=("$f")
                fi
            done
            source_files=("${unique_files[@]}")
        fi
    fi

    # Filter to testable source files only (exclude test files, configs, etc.)
    local -a testable_files=()
    for f in "${source_files[@]}"; do
        if _test_is_testable_file "$f"; then
            testable_files+=("$f")
        fi
    done

    if [ ${#testable_files[@]} -eq 0 ]; then
        if [ "$test_json" = true ]; then
            echo '{"status":"no_files","message":"No testable source files found","files_scanned":'"${#source_files[@]}"'}'
        else
            echo -e "${YELLOW}No testable source files found.${NC}"
            if [ "$test_changed" = true ]; then
                echo "Tip: Use --file or --dir to specify source files directly."
            fi
        fi
        return 1
    fi

    # --- Generate tests ---
    local total_files=${#testable_files[@]}
    local generated=0
    local skipped=0
    local -a generated_paths=()
    local json_entries=""

    if [ "$test_json" = false ] && [ "$test_dry_run" = false ]; then
        echo -e "${BOLD}Loki Test Generator${NC}"
        echo -e "Target coverage: ${CYAN}${test_coverage}%${NC}"
        echo -e "Files to process: ${CYAN}${total_files}${NC}"
        echo ""
    elif [ "$test_json" = false ] && [ "$test_dry_run" = true ]; then
        echo -e "${BOLD}Loki Test Generator (dry run)${NC}"
        echo -e "Files to process: ${CYAN}${total_files}${NC}"
        echo ""
    fi

    for src_file in "${testable_files[@]}"; do
        local lang=""
        local framework=""
        local test_path=""

        # Detect language
        lang=$(_test_detect_language "$src_file")
        if [ -z "$lang" ]; then
            ((++skipped))
            [ "$test_verbose" = true ] && echo -e "  ${DIM}Skip: $src_file (unsupported language)${NC}"
            continue
        fi

        # Detect or use specified framework
        if [ -n "$test_format" ] && [ "$test_format" != "auto" ]; then
            framework="$test_format"
        else
            framework=$(_test_detect_framework "$lang")
        fi

        # Determine test output path
        test_path=$(_test_output_path "$src_file" "$lang" "$framework" "$test_output")

        # Extract testable constructs
        local constructs=""
        constructs=$(_test_extract_constructs "$src_file" "$lang")

        if [ -z "$constructs" ]; then
            ((++skipped))
            [ "$test_verbose" = true ] && echo -e "  ${DIM}Skip: $src_file (no testable constructs found)${NC}"
            continue
        fi

        local construct_count
        construct_count=$(echo "$constructs" | wc -l | tr -d ' ')

        if [ "$test_dry_run" = true ]; then
            if [ "$test_json" = true ]; then
                local json_entry
                json_entry=$(printf '{"source":"%s","test_path":"%s","language":"%s","framework":"%s","constructs":%d}' \
                    "$src_file" "$test_path" "$lang" "$framework" "$construct_count")
                if [ -n "$json_entries" ]; then
                    json_entries="${json_entries},${json_entry}"
                else
                    json_entries="$json_entry"
                fi
            else
                echo -e "  ${CYAN}$src_file${NC}"
                echo -e "    -> ${GREEN}$test_path${NC}"
                echo -e "    Language: $lang | Framework: $framework | Constructs: $construct_count"
                if [ "$test_verbose" = true ]; then
                    echo "$constructs" | while IFS= read -r c; do
                        echo -e "      - $c"
                    done
                fi
            fi
            ((++generated))
            continue
        fi

        # Generate test content
        local test_content=""
        test_content=$(_test_generate_content "$src_file" "$lang" "$framework" "$constructs" "$test_coverage")

        # Write test file
        local test_dir_path
        test_dir_path=$(dirname "$test_path")
        mkdir -p "$test_dir_path"
        echo "$test_content" > "$test_path"

        generated_paths+=("$test_path")
        ((++generated))

        if [ "$test_json" = false ]; then
            echo -e "  ${GREEN}[generated]${NC} $test_path  (${construct_count} constructs, $framework)"
        else
            local json_entry
            json_entry=$(printf '{"source":"%s","test_path":"%s","language":"%s","framework":"%s","constructs":%d,"status":"generated"}' \
                "$src_file" "$test_path" "$lang" "$framework" "$construct_count")
            if [ -n "$json_entries" ]; then
                json_entries="${json_entries},${json_entry}"
            else
                json_entries="$json_entry"
            fi
        fi
    done

    # --- Output summary ---
    if [ "$test_json" = true ]; then
        local mode="generate"
        [ "$test_dry_run" = true ] && mode="dry_run"
        printf '{"status":"success","mode":"%s","files_scanned":%d,"tests_generated":%d,"skipped":%d,"coverage_target":%d,"results":[%s]}\n' \
            "$mode" "$total_files" "$generated" "$skipped" "$test_coverage" "$json_entries"
    else
        echo ""
        if [ "$test_dry_run" = true ]; then
            echo -e "${BOLD}Dry run summary:${NC} $generated test files would be generated, $skipped skipped"
        else
            echo -e "${BOLD}Summary:${NC} $generated test files generated, $skipped skipped"
            if [ ${#generated_paths[@]} -gt 0 ] && [ "$test_verbose" = true ]; then
                echo ""
                echo "Generated files:"
                for p in "${generated_paths[@]}"; do
                    echo "  $p"
                done
            fi
        fi
    fi

    if [ "$generated" -eq 0 ]; then
        return 1
    fi
    return 0
}

# --- Test generation helper functions ---

# Find source files in a directory (non-test, non-config files)
_test_find_source_files() {
    local dir="$1"
    find "$dir" -type f \( \
        -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \
        -o -name "*.py" -o -name "*.go" -o -name "*.rs" \
        -o -name "*.java" -o -name "*.rb" -o -name "*.sh" \
    \) ! -path "*/node_modules/*" ! -path "*/.git/*" ! -path "*/vendor/*" \
       ! -path "*/__pycache__/*" ! -path "*/target/*" ! -path "*/dist/*" \
       ! -path "*/build/*" ! -path "*/.loki/*" \
    2>/dev/null | sort
}

# Check if a file is a testable source file (not a test, config, or generated file)
_test_is_testable_file() {
    local f="$1"
    local base
    base=$(basename "$f")

    # Must be a recognized source file extension
    case "$f" in
        *.js|*.ts|*.jsx|*.tsx|*.py|*.go|*.rs|*.java|*.rb|*.sh) ;;
        *) return 1 ;;
    esac

    # Skip test files
    case "$base" in
        test_*|*_test.*|*.test.*|*.spec.*|*_spec.*|test.*) return 1 ;;
    esac
    case "$f" in
        */__tests__/*|*/tests/*|*/test/*|*/spec/*|*_test/*) return 1 ;;
    esac

    # Skip config and generated files
    case "$base" in
        setup.*|config.*|*.config.*|*.d.ts|__init__.py|conftest.py) return 1 ;;
        jest.config.*|vitest.config.*|webpack.config.*|rollup.config.*) return 1 ;;
        .eslintrc.*|.prettierrc.*|tsconfig.*|package.json) return 1 ;;
        Makefile|Dockerfile|*.lock|*.min.js|*.min.css) return 1 ;;
    esac

    return 0
}

# Detect language from file extension
_test_detect_language() {
    local f="$1"
    case "$f" in
        *.ts|*.tsx) echo "typescript" ;;
        *.js|*.jsx) echo "javascript" ;;
        *.py) echo "python" ;;
        *.go) echo "go" ;;
        *.rs) echo "rust" ;;
        *.java) echo "java" ;;
        *.rb) echo "ruby" ;;
        *.sh) echo "bash" ;;
        *) echo "" ;;
    esac
}

# Detect test framework based on language and project files
_test_detect_framework() {
    local lang="$1"
    case "$lang" in
        typescript|javascript)
            if [ -f "vitest.config.ts" ] || [ -f "vitest.config.js" ]; then
                echo "vitest"
            elif [ -f ".mocharc.yml" ] || [ -f ".mocharc.json" ] || [ -f ".mocharc.js" ]; then
                echo "mocha"
            elif [ -f "jest.config.ts" ] || [ -f "jest.config.js" ] || [ -f "jest.config.json" ]; then
                echo "jest"
            elif [ -f "package.json" ] && grep -q '"vitest"' package.json 2>/dev/null; then
                echo "vitest"
            elif [ -f "package.json" ] && grep -q '"jest"' package.json 2>/dev/null; then
                echo "jest"
            else
                echo "jest"
            fi
            ;;
        python) echo "pytest" ;;
        go) echo "go-test" ;;
        rust) echo "cargo-test" ;;
        java) echo "junit" ;;
        ruby) echo "rspec" ;;
        bash) echo "bats" ;;
        *) echo "unknown" ;;
    esac
}

# Determine output path for generated test file
_test_output_path() {
    local src="$1"
    local lang="$2"
    local framework="$3"
    local custom_output="$4"

    local dir
    local base
    local name
    local ext
    dir=$(dirname "$src")
    base=$(basename "$src")
    name="${base%.*}"
    ext="${base##*.}"

    # Use custom output dir if specified
    if [ -n "$custom_output" ]; then
        dir="$custom_output"
    fi

    case "$framework" in
        jest|vitest)
            # __tests__/ directory convention
            if [ -n "$custom_output" ]; then
                echo "${dir}/${name}.test.${ext}"
            else
                echo "${dir}/__tests__/${name}.test.${ext}"
            fi
            ;;
        mocha)
            if [ -n "$custom_output" ]; then
                echo "${dir}/${name}.spec.${ext}"
            else
                echo "${dir}/test/${name}.spec.${ext}"
            fi
            ;;
        pytest)
            if [ -n "$custom_output" ]; then
                echo "${dir}/test_${name}.py"
            else
                echo "${dir}/tests/test_${name}.py"
            fi
            ;;
        go-test)
            # Go tests live alongside source
            echo "${dir}/${name}_test.go"
            ;;
        cargo-test)
            # Rust tests in same file or tests/ dir
            if [ -n "$custom_output" ]; then
                echo "${dir}/${name}_test.rs"
            else
                echo "${dir}/tests/${name}_test.rs"
            fi
            ;;
        junit)
            if [ -n "$custom_output" ]; then
                echo "${dir}/${name}Test.java"
            else
                echo "${dir}/test/${name}Test.java"
            fi
            ;;
        rspec)
            if [ -n "$custom_output" ]; then
                echo "${dir}/${name}_spec.rb"
            else
                echo "${dir}/spec/${name}_spec.rb"
            fi
            ;;
        bats)
            if [ -n "$custom_output" ]; then
                echo "${dir}/${name}.bats"
            else
                echo "${dir}/tests/${name}.bats"
            fi
            ;;
        *)
            echo "${dir}/tests/test_${name}.${ext}"
            ;;
    esac
}

# Extract testable constructs from a source file (functions, classes, exports)
_test_extract_constructs() {
    local src="$1"
    local lang="$2"

    case "$lang" in
        javascript|typescript)
            # Match exported functions, classes, consts, and standalone functions
            grep -E '^\s*(export\s+)?(default\s+)?(async\s+)?function\s+\w+|^\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\(|^\s*(export\s+)?class\s+\w+|^module\.exports' "$src" 2>/dev/null \
                | sed 's/^\s*//' | head -50
            ;;
        python)
            # Match function definitions, class definitions
            grep -E '^\s*(async\s+)?def\s+\w+|^\s*class\s+\w+' "$src" 2>/dev/null \
                | grep -v '^\s*def\s+_' \
                | sed 's/^\s*//' | head -50
            ;;
        go)
            # Match function definitions (exported only - capitalized)
            grep -E '^func\s+(\([^)]+\)\s+)?[A-Z]\w*\(' "$src" 2>/dev/null \
                | head -50
            ;;
        rust)
            # Match pub functions and impl blocks
            grep -E '^\s*pub\s+(async\s+)?fn\s+\w+|^\s*impl\s+' "$src" 2>/dev/null \
                | sed 's/^\s*//' | head -50
            ;;
        java)
            # Match public methods and classes
            grep -E '^\s*public\s+(static\s+)?\w+\s+\w+\s*\(|^\s*public\s+class\s+\w+' "$src" 2>/dev/null \
                | sed 's/^\s*//' | head -50
            ;;
        ruby)
            # Match def and class
            grep -E '^\s*def\s+\w+|^\s*class\s+\w+' "$src" 2>/dev/null \
                | sed 's/^\s*//' | head -50
            ;;
        bash)
            # Match function definitions
            grep -E '^\s*\w+\s*\(\)\s*\{|^\s*function\s+\w+' "$src" 2>/dev/null \
                | sed 's/^\s*//' | head -50
            ;;
        *) echo "" ;;
    esac
}

# Generate test file content for a given source file
_test_generate_content() {
    local src="$1"
    local lang="$2"
    local framework="$3"
    local constructs="$4"
    local coverage="$5"

    local base
    local name
    base=$(basename "$src")
    name="${base%.*}"

    case "$framework" in
        jest|vitest)
            _test_gen_jest_vitest "$src" "$name" "$base" "$framework" "$constructs" "$coverage"
            ;;
        mocha)
            _test_gen_mocha "$src" "$name" "$base" "$constructs" "$coverage"
            ;;
        pytest)
            _test_gen_pytest "$src" "$name" "$base" "$constructs" "$coverage"
            ;;
        go-test)
            _test_gen_go "$src" "$name" "$base" "$constructs" "$coverage"
            ;;
        cargo-test)
            _test_gen_rust "$src" "$name" "$base" "$constructs" "$coverage"
            ;;
        junit)
            _test_gen_junit "$src" "$name" "$base" "$constructs" "$coverage"
            ;;
        rspec)
            _test_gen_rspec "$src" "$name" "$base" "$constructs" "$coverage"
            ;;
        bats)
            _test_gen_bats "$src" "$name" "$base" "$constructs" "$coverage"
            ;;
        *)
            echo "// Unsupported framework: $framework"
            ;;
    esac
}

# --- Framework-specific generators ---

_test_gen_jest_vitest() {
    local src="$1" name="$2" base="$3" framework="$4" constructs="$5" coverage="$6"

    local import_style="require"
    local ext="${base##*.}"
    if [[ "$ext" == "ts" || "$ext" == "tsx" ]]; then
        import_style="import"
    fi

    # Header
    if [ "$framework" = "vitest" ]; then
        echo "import { describe, it, expect, beforeEach, afterEach } from 'vitest';"
    fi

    # Import statement
    local rel_path
    rel_path=$(echo "$src" | sed 's|^\./||')
    local import_path="../${base%.*}"

    if [ "$import_style" = "import" ]; then
        echo "import { /* TODO: import constructs */ } from '${import_path}';"
    else
        echo "const ${name} = require('${import_path}');"
    fi
    echo ""
    echo "// Generated test suite for ${base}"
    echo "// Target coverage: ${coverage}%"
    echo "// Source: ${src}"
    echo ""
    echo "describe('${name}', () => {"

    # Generate test cases from constructs
    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        # Skip module.exports lines
        echo "$construct" | grep -q '^module\.exports' && continue
        local func_name=""
        # Extract function/class name
        func_name=$(echo "$construct" | grep -oE '(function|const|let|var|class)\s+\w+' | awk '{print $2}')
        if [ -z "$func_name" ]; then
            func_name=$(echo "$construct" | grep -oE '\w+\s*=' | sed 's/\s*=$//')
        fi
        [ -z "$func_name" ] && continue

        # Check if it's a class
        if echo "$construct" | grep -q 'class\s'; then
            echo ""
            echo "  describe('${func_name}', () => {"
            echo "    it('should be instantiable', () => {"
            echo "      // TODO: const instance = new ${func_name}();"
            echo "      // expect(instance).toBeDefined();"
            echo "    });"
            echo ""
            echo "    it('should have expected properties', () => {"
            echo "      // TODO: verify instance properties"
            echo "    });"
            echo "  });"
        else
            echo ""
            echo "  describe('${func_name}', () => {"
            echo "    it('should return expected result for valid input', () => {"
            echo "      // TODO: const result = ${func_name}(/* args */);"
            echo "      // expect(result).toEqual(/* expected */);"
            echo "    });"
            echo ""
            echo "    it('should handle edge cases', () => {"
            echo "      // TODO: test with null, undefined, empty, boundary values"
            echo "    });"
            echo ""
            echo "    it('should throw on invalid input', () => {"
            echo "      // TODO: expect(() => ${func_name}(/* invalid */)).toThrow();"
            echo "    });"
            echo "  });"
        fi
    done

    echo "});"
}

_test_gen_mocha() {
    local src="$1" name="$2" base="$3" constructs="$4" coverage="$5"

    echo "const { expect } = require('chai');"
    echo "const ${name} = require('../${name}');"
    echo ""
    echo "// Generated test suite for ${base}"
    echo "// Target coverage: ${coverage}%"
    echo ""
    echo "describe('${name}', function () {"

    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        echo "$construct" | grep -q '^module\.exports' && continue
        local func_name=""
        func_name=$(echo "$construct" | grep -oE '(function|const|let|var|class)\s+\w+' | awk '{print $2}')
        [ -z "$func_name" ] && continue

        echo ""
        echo "  describe('${func_name}', function () {"
        echo "    it('should return expected result', function () {"
        echo "      // TODO: const result = ${name}.${func_name}(/* args */);"
        echo "      // expect(result).to.equal(/* expected */);"
        echo "    });"
        echo ""
        echo "    it('should handle edge cases', function () {"
        echo "      // TODO: test edge cases"
        echo "    });"
        echo "  });"
    done

    echo "});"
}

_test_gen_pytest() {
    local src="$1" name="$2" base="$3" constructs="$4" coverage="$5"

    local module_name
    module_name=$(echo "$name" | tr '-' '_')

    echo "\"\"\"Tests for ${base}."
    echo ""
    echo "Target coverage: ${coverage}%"
    echo "Source: ${src}"
    echo "\"\"\""
    echo "import pytest"
    echo "# from ${module_name} import  # TODO: import from source module"
    echo ""

    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        local func_name=""

        if echo "$construct" | grep -q '^\s*class\s'; then
            func_name=$(echo "$construct" | grep -oE 'class\s+\w+' | awk '{print $2}')
            [ -z "$func_name" ] && continue
            echo ""
            echo "class Test${func_name}:"
            echo "    \"\"\"Tests for ${func_name} class.\"\"\""
            echo ""
            echo "    def test_init(self):"
            echo "        \"\"\"Test ${func_name} instantiation.\"\"\""
            echo "        # TODO: instance = ${func_name}()"
            echo "        # assert instance is not None"
            echo "        pass"
            echo ""
            echo "    def test_expected_behavior(self):"
            echo "        \"\"\"Test ${func_name} expected behavior.\"\"\""
            echo "        # TODO: implement"
            echo "        pass"
        else
            func_name=$(echo "$construct" | grep -oE 'def\s+\w+' | awk '{print $2}')
            [ -z "$func_name" ] && continue
            echo ""
            echo "def test_${func_name}_valid_input():"
            echo "    \"\"\"Test ${func_name} with valid input.\"\"\""
            echo "    # TODO: result = ${func_name}(/* args */)"
            echo "    # assert result == expected"
            echo "    pass"
            echo ""
            echo ""
            echo "def test_${func_name}_edge_cases():"
            echo "    \"\"\"Test ${func_name} edge cases.\"\"\""
            echo "    # TODO: test with None, empty, boundary values"
            echo "    pass"
            echo ""
            echo ""
            echo "def test_${func_name}_invalid_input():"
            echo "    \"\"\"Test ${func_name} with invalid input.\"\"\""
            echo "    # TODO: with pytest.raises(ValueError):"
            echo "    #     ${func_name}(/* invalid */)"
            echo "    pass"
        fi
    done
}

_test_gen_go() {
    local src="$1" name="$2" base="$3" constructs="$4" coverage="$5"

    local pkg
    pkg=$(grep -m1 '^package\s' "$src" 2>/dev/null | awk '{print $2}')
    [ -z "$pkg" ] && pkg="main"

    echo "package ${pkg}"
    echo ""
    echo "import ("
    echo "	\"testing\""
    echo ")"
    echo ""
    echo "// Generated tests for ${base}"
    echo "// Target coverage: ${coverage}%"

    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        local func_name=""
        func_name=$(echo "$construct" | grep -oE 'func\s+(\([^)]+\)\s+)?\w+' | grep -oE '\w+$')
        [ -z "$func_name" ] && continue

        echo ""
        echo "func Test${func_name}(t *testing.T) {"
        echo "	t.Run(\"valid input\", func(t *testing.T) {"
        echo "		// TODO: result := ${func_name}(/* args */)"
        echo "		// if result != expected {"
        echo "		// 	t.Errorf(\"${func_name}() = %v, want %v\", result, expected)"
        echo "		// }"
        echo "	})"
        echo ""
        echo "	t.Run(\"edge cases\", func(t *testing.T) {"
        echo "		// TODO: test boundary values"
        echo "	})"
        echo "}"
    done
}

_test_gen_rust() {
    local src="$1" name="$2" base="$3" constructs="$4" coverage="$5"

    echo "// Generated tests for ${base}"
    echo "// Target coverage: ${coverage}%"
    echo ""
    echo "#[cfg(test)]"
    echo "mod tests {"
    echo "    use super::*;"
    echo ""

    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        local func_name=""
        func_name=$(echo "$construct" | grep -oE 'fn\s+\w+' | awk '{print $2}')
        [ -z "$func_name" ] && continue

        echo "    #[test]"
        echo "    fn test_${func_name}_valid_input() {"
        echo "        // TODO: let result = ${func_name}(/* args */);"
        echo "        // assert_eq!(result, expected);"
        echo "    }"
        echo ""
        echo "    #[test]"
        echo "    fn test_${func_name}_edge_cases() {"
        echo "        // TODO: test boundary values"
        echo "    }"
        echo ""
    done

    echo "}"
}

_test_gen_junit() {
    local src="$1" name="$2" base="$3" constructs="$4" coverage="$5"

    echo "import org.junit.jupiter.api.Test;"
    echo "import org.junit.jupiter.api.BeforeEach;"
    echo "import static org.junit.jupiter.api.Assertions.*;"
    echo ""
    echo "/**"
    echo " * Generated tests for ${base}"
    echo " * Target coverage: ${coverage}%"
    echo " */"
    echo "class ${name}Test {"
    echo ""

    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        local func_name=""
        func_name=$(echo "$construct" | grep -oE '(class|void|int|String|boolean|double|float|long)\s+\w+' | awk '{print $NF}')
        [ -z "$func_name" ] && continue

        # Skip class definitions
        if echo "$construct" | grep -q 'class\s'; then
            continue
        fi

        echo "    @Test"
        echo "    void test${func_name^}ValidInput() {"
        echo "        // TODO: var result = instance.${func_name}(/* args */);"
        echo "        // assertEquals(expected, result);"
        echo "    }"
        echo ""
        echo "    @Test"
        echo "    void test${func_name^}EdgeCases() {"
        echo "        // TODO: test boundary values"
        echo "    }"
        echo ""
    done

    echo "}"
}

_test_gen_rspec() {
    local src="$1" name="$2" base="$3" constructs="$4" coverage="$5"

    echo "# Generated tests for ${base}"
    echo "# Target coverage: ${coverage}%"
    echo ""
    echo "require_relative '../${name}'"
    echo ""
    echo "RSpec.describe ${name^} do"

    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        local func_name=""

        if echo "$construct" | grep -q 'class\s'; then
            func_name=$(echo "$construct" | grep -oE 'class\s+\w+' | awk '{print $2}')
            [ -z "$func_name" ] && continue
            echo ""
            echo "  describe ${func_name} do"
            echo "    it 'is instantiable' do"
            echo "      # TODO: instance = ${func_name}.new"
            echo "      # expect(instance).not_to be_nil"
            echo "    end"
            echo "  end"
        else
            func_name=$(echo "$construct" | grep -oE 'def\s+\w+' | awk '{print $2}')
            [ -z "$func_name" ] && continue
            echo ""
            echo "  describe '#${func_name}' do"
            echo "    it 'returns expected result' do"
            echo "      # TODO: result = subject.${func_name}"
            echo "      # expect(result).to eq(expected)"
            echo "    end"
            echo ""
            echo "    it 'handles edge cases' do"
            echo "      # TODO: test edge cases"
            echo "    end"
            echo "  end"
        fi
    done

    echo "end"
}

_test_gen_bats() {
    local src="$1" name="$2" base="$3" constructs="$4" coverage="$5"

    echo "#!/usr/bin/env bats"
    echo "# Generated tests for ${base}"
    echo "# Target coverage: ${coverage}%"
    echo ""
    echo "setup() {"
    echo "    source \"\$(dirname \"\$BATS_TEST_FILENAME\")/../${base}\""
    echo "}"
    echo ""

    echo "$constructs" | while IFS= read -r construct; do
        [ -z "$construct" ] && continue
        local func_name=""
        func_name=$(echo "$construct" | grep -oE '\w+' | head -1)
        [ -z "$func_name" ] && continue

        echo "@test \"${func_name} returns expected result\" {"
        echo "    # TODO: run ${func_name} args"
        echo "    # [ \"\$status\" -eq 0 ]"
        echo "    # [ \"\$output\" = \"expected\" ]"
        echo "    skip \"TODO: implement\""
        echo "}"
        echo ""
        echo "@test \"${func_name} handles edge cases\" {"
        echo "    # TODO: test edge cases"
        echo "    skip \"TODO: implement\""
        echo "}"
        echo ""
    done
}

# Session report generator (v6.27.0)
# CLI consolidation (Phase A): `report` noun dispatcher.
# Groups the read-only reporting cluster under one noun:
#   report session  -> cmd_stats       (session statistics)
#   report metrics  -> cmd_metrics     (productivity report)
#   report cost     -> cmd_cost        (cost + budget view)
#   report export   -> cmd_export      (session data export)
#   report share    -> cmd_share       (share report as Gist)
#   report dogfood  -> cmd_dogfood     (self-development stats)
# Any other first token (including a flag like --format, or no token) falls
# through to the legacy session-report generator (cmd_report_legacy) so that the
# pre-existing `loki report [--format ...]` surface is byte-for-byte unchanged.
# No handler logic moves in Phase A: each subcommand calls the existing bare
# cmd_* function so the canonical `report <sub>` output is identical to the
# legacy top-level command. The deprecation pointer for the old top-level names
# is emitted at the alias dispatch arm (main()), never here, so `report <sub>`
# stays clean.
cmd_report() {
    # CLI consolidation (Phase B): `report kpis` is the canonical KPI snapshot
    # entry. kpis is Bun-only; on a machine WITH Bun, bin/loki two-token-routes
    # `report kpis` (flag-anywhere) to the Bun runtime before this function is
    # reached. This is the bash-route (LOKI_LEGACY_BASH / no-Bun) path: print
    # the honest requirement message, NO deprecation pointer (this IS the
    # canonical form), exit 1. `kpis` is the report subcommand ONLY when it is the
    # FIRST non-flag token after `report` (so `report kpis --json` and `report
    # --json kpis` both route here, mirroring the bin/loki + Bun cli.ts routing),
    # NOT when it appears anywhere. A `kpis` token that is a positional VALUE of a
    # different report subcommand -- e.g. `report export json kpis`, where `kpis`
    # is the export OUTPUT FILENAME -- must NOT be hijacked; it falls through to
    # the export arm below and behaves exactly as on main (v7.31.0): exit 0, file
    # `kpis` created. Only the subcommand token is stripped before forwarding to
    # the helper; flags (e.g. --json) are passed through so it still emits the
    # structured machine payload.
    local _report_first_sub="" _kpis_args=() _dropped_kpis=""
    local _a
    for _a in "$@"; do
        case "$_a" in
            -*) _kpis_args+=("$_a") ;;
            *)
                if [ -z "$_report_first_sub" ]; then
                    _report_first_sub="$_a"
                    # Drop only the subcommand token itself, once.
                    if [ "$_a" = "kpis" ]; then
                        _dropped_kpis=1
                        continue
                    fi
                fi
                _kpis_args+=("$_a")
                ;;
        esac
    done
    if [ -n "$_dropped_kpis" ]; then
        # ${arr[@]:-} guards the empty-array expansion under `set -u` on the
        # bare `report kpis` case (no extra flags) for older bash (3.2/4.2).
        _kpis_bash_unavailable "${_kpis_args[@]:-}"
    fi
    case "${1:-}" in
        session)  shift; cmd_stats "$@";   return $? ;;
        metrics)  shift; cmd_metrics "$@"; return $? ;;
        cost)     shift; cmd_cost "$@";    return $? ;;
        export)   shift; cmd_export "$@";  return $? ;;
        share)    shift; cmd_share "$@";   return $? ;;
        dogfood)  shift; cmd_dogfood "$@"; return $? ;;
        release-notes)
            shift
            # User release notes (#391): every commit in <from>..<to> is listed under
            # "changes" when its Loki-Run trailer names a run whose receipt.json
            # (schema loki.v10.receipt/1) exists under .loki/runs, else under "not sealed".
            local _rn_range="" _rn_fmt="text" _rn_a
            while [ $# -gt 0 ]; do
                _rn_a="$1"; shift
                case "$_rn_a" in
                    --format) _rn_fmt="${1:-}"; [ $# -gt 0 ] && shift ;;
                    --format=*) _rn_fmt="${_rn_a#--format=}" ;;
                    -*) _rn_range="!"; break ;;
                    *) [ -z "$_rn_range" ] && _rn_range="$_rn_a" || _rn_range="!" ;;
                esac
            done
            case "$_rn_fmt" in text|json) ;; *) _rn_range="!" ;; esac
            case "$_rn_range" in
                ?*..?*) ;;
                *) echo "Usage: loki report release-notes <from>..<to> [--format text|json]" >&2; return 2 ;;
            esac
            local _rn_log
            _rn_log="$(git log --format='%H%x1f%s%x1f%(trailers:key=Loki-Run,valueonly,separator=%x2c)%x1e' "$_rn_range" 2>/dev/null)" || {
                echo "Usage: loki report release-notes <from>..<to> [--format text|json]" >&2
                echo "release-notes: cannot resolve range: $_rn_range" >&2
                return 2
            }
            _RN_LOG="$_rn_log" python3 - "$_rn_fmt" "${LOKI_DIR:-.loki}" <<'PYRN'
import json, os, sys
fmt, loki_dir = sys.argv[1], sys.argv[2]
changes, unsealed = [], []
for rec in os.environ.get("_RN_LOG", "").split("\x1e"):
    rec = rec.strip("\n")
    if not rec:
        continue
    sha, subject, run = (rec.split("\x1f") + ["", ""])[:3]
    run = run.strip().split(",")[0].strip()
    receipt = ""
    if run and "/" not in run and ".." not in run:
        try:
            with open(os.path.join(loki_dir, "runs", run, "receipt.json")) as fh:
                if json.load(fh).get("schema") == "loki.v10.receipt/1":
                    receipt = run
        except (OSError, ValueError, AttributeError):
            pass
    row = {"commit": sha[:12], "subject": subject}
    (changes if receipt else unsealed).append(dict(row, receipt=receipt) if receipt else row)
if fmt == "json":
    print(json.dumps({"changes": changes, "not_sealed": unsealed}, indent=2))
else:
    print("changes (%d)" % len(changes))
    for r in changes:
        print("  %s  %s  [receipt %s]" % (r["commit"], r["subject"], r["receipt"]))
    print("not sealed (%d)" % len(unsealed))
    for r in unsealed:
        print("  %s  %s" % (r["commit"], r["subject"]))
PYRN
            return $? ;;
    esac
    cmd_report_legacy "$@"
}

cmd_report_legacy() {
    local session_id=""
    local format="text"
    local output_file=""
    local include_gates=true
    local include_agents=true
    local include_timeline=true

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki report${NC} - Session report generator"
                echo ""
                echo "Usage: loki report [subcommand] [options]"
                echo ""
                echo "Generates a readable session report from .loki/ data."
                echo ""
                echo "Subcommands (grouped reporting surface, v7.31):"
                echo "  session     Session statistics (was: loki stats)"
                echo "  metrics     Efficiency/reward metrics (was: loki metrics)"
                echo "  cost        Token cost breakdown (was: loki cost)"
                echo "  kpis        Accuracy + efficiency KPI snapshot (was: loki kpis; Bun runtime only)"
                echo "  export      Export session data (was: loki export) [json|markdown|csv|timeline]"
                echo "  share       Share session assets (was: loki share)"
                echo "  dogfood     Self-development stats (was: loki dogfood)"
                echo "  release-notes <from>..<to> [--format text|json]  Changes in a range; \"sealed\" reads only local .loki/runs receipts"
                echo "  (Run 'loki report <subcommand> --help' for each. The old"
                echo "   top-level names still work; see 'loki help aliases'.)"
                echo ""
                echo "With no subcommand, generates the legacy session report below."
                echo ""
                echo "Options:"
                echo "  --session <id>       Report for specific session (default: latest)"
                echo "  --format <fmt>       Output format: text, markdown, html (default: text)"
                echo "  --output <file>      Save to file (default: stdout)"
                echo "  --no-gates           Exclude quality gate details"
                echo "  --no-agents          Exclude agent activity"
                echo "  --no-timeline        Exclude event timeline"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Examples:"
                echo "  loki report                            # Text report to stdout"
                echo "  loki report --format markdown          # Markdown report"
                echo "  loki report --format html -o report.html  # HTML to file"
                echo "  loki report --no-gates --no-agents     # Minimal report"
                exit 0
                ;;
            --session) session_id="${2:-}"; shift 2 ;;
            --session=*) session_id="${1#*=}"; shift ;;
            --format) format="${2:-text}"; shift 2 ;;
            --format=*) format="${1#*=}"; shift ;;
            --output|-o) output_file="${2:-}"; shift 2 ;;
            --output=*) output_file="${1#*=}"; shift ;;
            --no-gates) include_gates=false; shift ;;
            --no-agents) include_agents=false; shift ;;
            --no-timeline) include_timeline=false; shift ;;
            --include-gates) include_gates=true; shift ;;
            --include-agents) include_agents=true; shift ;;
            --include-timeline) include_timeline=true; shift ;;
            *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki report --help' for usage." >&2; exit 1 ;;  # stderr for noun-family error-channel parity (analyze/modernize)
        esac
    done

    case "$format" in
        text|markdown|md|html) ;;
        *) echo -e "${RED}Unknown format: $format${NC}"; echo "Supported: text, markdown, html"; exit 1 ;;
    esac

    # Normalize md -> markdown
    [ "$format" = "md" ] && format="markdown"

    if ! command -v python3 &>/dev/null; then
        echo -e "${RED}python3 is required for report generation${NC}"
        exit 1
    fi

    local report_output
    report_output=$(LOKI_DIR="${LOKI_DIR:-.loki}" \
        REPORT_FORMAT="$format" \
        REPORT_SESSION="$session_id" \
        REPORT_GATES="$include_gates" \
        REPORT_AGENTS="$include_agents" \
        REPORT_TIMELINE="$include_timeline" \
        python3 << 'REPORT_SCRIPT'
import json
import os
import glob
from datetime import datetime, timezone

loki_dir = os.environ.get("LOKI_DIR", ".loki")
fmt = os.environ.get("REPORT_FORMAT", "text")
session_id = os.environ.get("REPORT_SESSION", "")
show_gates = os.environ.get("REPORT_GATES", "true") == "true"
show_agents = os.environ.get("REPORT_AGENTS", "true") == "true"
show_timeline = os.environ.get("REPORT_TIMELINE", "true") == "true"

def load_json(path):
    try:
        with open(path) as f:
            return json.load(f)
    except:
        return None

def load_text(path):
    try:
        with open(path) as f:
            return f.read().strip()
    except:
        return None

def format_duration(start_str, end_str=None):
    try:
        start = datetime.fromisoformat(start_str.replace("Z", "+00:00"))
        if end_str:
            end = datetime.fromisoformat(end_str.replace("Z", "+00:00"))
        else:
            end = datetime.now(timezone.utc)
        delta = end - start
        hours, remainder = divmod(int(delta.total_seconds()), 3600)
        minutes, seconds = divmod(remainder, 60)
        if hours > 0:
            return f"{hours}h {minutes}m {seconds}s"
        elif minutes > 0:
            return f"{minutes}m {seconds}s"
        else:
            return f"{seconds}s"
    except:
        return "unknown"

# --- Gather data ---

# Session/autonomy state
autonomy = load_json(os.path.join(loki_dir, "autonomy-state.json")) or {}
orchestrator = load_json(os.path.join(loki_dir, "state", "orchestrator.json")) or {}
metrics = load_json(os.path.join(loki_dir, "state", "metrics.json")) or {}
config = load_json(os.path.join(loki_dir, "config.json")) or {}
dashboard_state = load_json(os.path.join(loki_dir, "dashboard-state.json")) or {}

# Project info
project_name = config.get("project_name", "")
if not project_name:
    # Try to get from git or cwd
    try:
        project_name = os.path.basename(os.getcwd())
    except:
        project_name = "Unknown Project"

# Timestamps
started_at = orchestrator.get("startedAt", autonomy.get("lastRun", ""))
status = autonomy.get("status", orchestrator.get("currentPhase", "unknown"))
version = orchestrator.get("version", "")
current_phase = orchestrator.get("currentPhase", "unknown")
retry_count = autonomy.get("retryCount", 0)

# Provider
provider_file = os.path.join(loki_dir, "state", "provider")
provider = "unknown"
if os.path.isdir(provider_file):
    for pf in glob.glob(os.path.join(provider_file, "*.json")):
        pd = load_json(pf)
        if pd and pd.get("name"):
            provider = pd["name"]
            break
elif os.path.isfile(provider_file):
    provider = load_text(provider_file) or "unknown"

# PRD
prd_path = autonomy.get("prdPath", "")
prd_summary = ""
if prd_path and os.path.exists(prd_path):
    prd_text = load_text(prd_path) or ""
    # Extract first non-empty, non-heading line as summary
    for line in prd_text.split("\n"):
        stripped = line.strip()
        if stripped and not stripped.startswith("#") and len(stripped) > 20:
            prd_summary = stripped[:200]
            break

# Quality gates
gate_failures = load_json(os.path.join(loki_dir, "quality", "gate-failure-count.json")) or {}
test_results = load_json(os.path.join(loki_dir, "quality", "test-results.json")) or {}
static_analysis = load_json(os.path.join(loki_dir, "quality", "static-analysis.json")) or {}
gate_failure_text = load_text(os.path.join(loki_dir, "quality", "gate-failures.txt")) or ""

# Reviews
reviews_dir = os.path.join(loki_dir, "quality", "reviews")
review_files = []
if os.path.isdir(reviews_dir):
    review_files = sorted(glob.glob(os.path.join(reviews_dir, "*.json")), reverse=True)[:5]

# Agents
agents = load_json(os.path.join(loki_dir, "state", "agents.json")) or []
if isinstance(agents, dict):
    agents = list(agents.values()) if agents else []

# Agent audit log
audit_log = os.path.join(loki_dir, "logs", "agent-audit.jsonl")
audit_entries = []
if os.path.exists(audit_log):
    try:
        with open(audit_log) as f:
            for line in f:
                try:
                    audit_entries.append(json.loads(line.strip()))
                except:
                    pass
    except:
        pass

# Queue stats
queue_completed = load_json(os.path.join(loki_dir, "queue", "completed.json")) or []
queue_failed = load_json(os.path.join(loki_dir, "queue", "failed.json")) or []
queue_pending = load_json(os.path.join(loki_dir, "queue", "pending.json")) or []
queue_in_progress = load_json(os.path.join(loki_dir, "queue", "in-progress.json")) or []

# Ensure lists
if isinstance(queue_completed, dict): queue_completed = queue_completed.get("tasks", [])
if isinstance(queue_failed, dict): queue_failed = queue_failed.get("tasks", [])
if isinstance(queue_pending, dict): queue_pending = queue_pending.get("tasks", [])
if isinstance(queue_in_progress, dict): queue_in_progress = queue_in_progress.get("tasks", [])

# Council
council_state = load_json(os.path.join(loki_dir, "council", "state.json")) or {}

# Events
events_file = os.path.join(loki_dir, "events.jsonl")
events = []
if os.path.exists(events_file):
    try:
        with open(events_file) as f:
            for line in f:
                try:
                    events.append(json.loads(line.strip()))
                except:
                    pass
    except:
        pass

# Git diff stats (run git command)
git_stats = ""
try:
    import subprocess
    result = subprocess.run(
        ["git", "diff", "--stat", "HEAD~1", "HEAD"],
        capture_output=True, text=True, timeout=10
    )
    if result.returncode == 0 and result.stdout.strip():
        git_stats = result.stdout.strip()
except:
    pass

# --- Build report sections ---

sections = []

# 1. Header
header_lines = []
header_lines.append(f"Project: {project_name}")
if version:
    header_lines.append(f"Version: {version}")
header_lines.append(f"Status: {status}")
if started_at:
    header_lines.append(f"Started: {started_at}")
    header_lines.append(f"Duration: {format_duration(started_at)}")
if provider != "unknown":
    header_lines.append(f"Provider: {provider}")
if current_phase != "unknown":
    header_lines.append(f"Phase: {current_phase}")
sections.append(("Session Overview", header_lines))

# 2. Executive Summary
summary_lines = []
tasks_done = len(queue_completed) if isinstance(queue_completed, list) else 0
tasks_failed_count = len(queue_failed) if isinstance(queue_failed, list) else 0
tasks_total = tasks_done + tasks_failed_count + (len(queue_pending) if isinstance(queue_pending, list) else 0) + (len(queue_in_progress) if isinstance(queue_in_progress, list) else 0)

if tasks_total > 0:
    summary_lines.append(f"Tasks: {tasks_done} completed, {tasks_failed_count} failed, {tasks_total} total")
else:
    summary_lines.append("No task queue data available")

summary_lines.append(f"Iterations: {retry_count}")

mc = metrics.get("tasks_completed", 0)
mf = metrics.get("tasks_failed", 0)
if mc or mf:
    summary_lines.append(f"Metrics: {mc} tasks completed, {mf} failed")

loc_added = metrics.get("lines_of_code_added", 0)
loc_test = metrics.get("lines_of_test_added", 0)
if loc_added or loc_test:
    summary_lines.append(f"Lines added: {loc_added} code, {loc_test} test")

council_votes = council_state.get("total_votes", 0)
if council_votes:
    approve = council_state.get("approve_votes", 0)
    reject = council_state.get("reject_votes", 0)
    summary_lines.append(f"Council: {council_votes} votes ({approve} approve, {reject} reject)")

if prd_summary:
    summary_lines.append(f"PRD: {prd_summary}")

sections.append(("Executive Summary", summary_lines))

# 3. RARV Cycles
rarv_lines = []
rarv_lines.append(f"Total iterations: {retry_count}")
rarv_lines.append(f"Max retries: {autonomy.get('maxRetries', 'N/A')}")
rarv_lines.append(f"Current phase: {current_phase}")
if autonomy.get("lastExitCode") is not None:
    rarv_lines.append(f"Last exit code: {autonomy.get('lastExitCode')}")
sections.append(("RARV Cycles", rarv_lines))

# 4. Agent Activity
if show_agents:
    agent_lines = []
    if agents:
        for a in agents:
            if isinstance(a, dict):
                aid = a.get("agent_id", a.get("agent_type", "unknown"))
                atype = a.get("agent_type", "")
                astatus = a.get("status", "unknown")
                model = a.get("model", "")
                completed = a.get("tasks_completed", [])
                task_count = len(completed) if isinstance(completed, list) else completed
                desc = f"{aid}"
                if atype:
                    desc += f" ({atype})"
                if model:
                    desc += f" [{model}]"
                desc += f" - {astatus}"
                if task_count:
                    desc += f", {task_count} tasks done"
                agent_lines.append(desc)
    else:
        agent_lines.append("No agent data recorded")

    if audit_entries:
        # Summarize by action type
        action_counts = {}
        for entry in audit_entries:
            action = entry.get("action", "unknown")
            action_counts[action] = action_counts.get(action, 0) + 1
        agent_lines.append("")
        agent_lines.append(f"Audit log: {len(audit_entries)} entries")
        for action, count in sorted(action_counts.items(), key=lambda x: -x[1])[:10]:
            agent_lines.append(f"  {action}: {count}")

    sections.append(("Agent Activity", agent_lines))

# 5. Quality Gates
if show_gates:
    gate_lines = []

    gate_names = [
        "static_analysis", "test_coverage", "code_review",
        "security_scan", "lint", "type_check",
        "integration_test", "e2e_test", "performance"
    ]

    if gate_failures:
        for gate in gate_names:
            failures = gate_failures.get(gate, 0)
            if failures > 0:
                gate_lines.append(f"  {gate}: {failures} failure(s)")
            else:
                gate_lines.append(f"  {gate}: PASS")
        if not any(gate_failures.get(g, 0) for g in gate_names):
            # Check if we have any non-standard gates
            for g, c in gate_failures.items():
                if g not in gate_names:
                    gate_lines.append(f"  {g}: {c} failure(s)")
    else:
        gate_lines.append("No quality gate data recorded")

    if test_results:
        gate_lines.append("")
        runner = test_results.get("runner", "unknown")
        passed = test_results.get("pass", "unknown")
        gate_lines.append(f"Test runner: {runner}")
        gate_lines.append(f"Tests passed: {passed}")
        if test_results.get("summary"):
            gate_lines.append(f"Summary: {test_results['summary'][:200]}")

    if gate_failure_text:
        gate_lines.append("")
        gate_lines.append("Gate failure details:")
        for line in gate_failure_text.split("\n")[:10]:
            gate_lines.append(f"  {line}")

    sections.append(("Quality Gates", gate_lines))

# 6. What Changed
change_lines = []
if git_stats:
    for line in git_stats.split("\n"):
        change_lines.append(line)
else:
    change_lines.append("No git diff data available (run from project root)")
sections.append(("What Changed", change_lines))

# 7. Tests
test_lines = []
if test_results:
    test_lines.append(f"Runner: {test_results.get('runner', 'unknown')}")
    test_lines.append(f"Passed: {test_results.get('pass', 'unknown')}")
    test_lines.append(f"Timestamp: {test_results.get('timestamp', 'unknown')}")
    if test_results.get("min_coverage"):
        test_lines.append(f"Min coverage TARGET (not measured): {test_results['min_coverage']}%")
else:
    test_lines.append("No test result data recorded")
sections.append(("Tests", test_lines))

# 8. Key Decisions (from events and council)
decision_lines = []
verdicts = council_state.get("verdicts", [])
if verdicts:
    for v in verdicts[-10:]:
        result = v.get("result", "UNKNOWN")
        iteration = v.get("iteration", "?")
        ts = v.get("timestamp", "")
        approve = v.get("approve", 0)
        reject = v.get("reject", 0)
        decision_lines.append(f"Iteration {iteration}: {result} ({approve} approve / {reject} reject) {ts}")

stagnation = council_state.get("consecutive_no_change", 0)
if stagnation > 0:
    decision_lines.append(f"Stagnation streak: {stagnation} iterations with no change")

done_signals = council_state.get("done_signals", 0)
if done_signals > 0:
    decision_lines.append(f"Done signals detected: {done_signals}")

if not decision_lines:
    decision_lines.append("No key decisions recorded")
sections.append(("Key Decisions", decision_lines))

# 9. Timeline (from events)
if show_timeline and events:
    timeline_lines = []
    for evt in events[-20:]:
        ts = evt.get("timestamp", evt.get("ts", ""))
        etype = evt.get("type", evt.get("event", "unknown"))
        data = evt.get("data", evt.get("payload", {}))
        summary = ""
        if isinstance(data, dict):
            summary = data.get("message", data.get("description", json.dumps(data)[:80]))
        elif isinstance(data, str):
            summary = data[:80]
        if ts:
            timeline_lines.append(f"[{ts}] {etype}: {summary}")
        else:
            timeline_lines.append(f"{etype}: {summary}")
    if timeline_lines:
        sections.append(("Timeline", timeline_lines))

# --- Render ---

def render_text(sections):
    lines = []
    width = 72
    lines.append("=" * width)
    lines.append("  LOKI MODE SESSION REPORT")
    lines.append("=" * width)
    lines.append("")
    for title, content in sections:
        lines.append(f"--- {title} ---")
        for line in content:
            lines.append(f"  {line}")
        lines.append("")
    lines.append("-" * width)
    lines.append(f"  Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
    lines.append(f"  Loki Mode v{version or 'unknown'}")
    lines.append("-" * width)
    return "\n".join(lines)

def render_markdown(sections):
    lines = []
    lines.append("# Loki Mode Session Report")
    lines.append("")
    for title, content in sections:
        lines.append(f"## {title}")
        lines.append("")
        for line in content:
            if line.strip() == "":
                lines.append("")
            elif line.strip().startswith("  "):
                lines.append(f"- {line.strip()}")
            else:
                lines.append(line)
        lines.append("")
    lines.append("---")
    lines.append(f"*Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')} | Loki Mode v{version or 'unknown'}*")
    return "\n".join(lines)

def render_html(sections):
    lines = []
    lines.append("<!DOCTYPE html>")
    lines.append("<html lang=\"en\">")
    lines.append("<head>")
    lines.append("<meta charset=\"UTF-8\">")
    lines.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">")
    lines.append("<title>Loki Mode Session Report</title>")
    lines.append("<style>")
    lines.append("""
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    max-width: 800px;
    margin: 40px auto;
    padding: 0 20px;
    background: #0d1117;
    color: #c9d1d9;
    line-height: 1.6;
}
h1 {
    color: #58a6ff;
    border-bottom: 2px solid #30363d;
    padding-bottom: 12px;
}
h2 {
    color: #8b949e;
    font-size: 1.1em;
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-top: 32px;
    border-bottom: 1px solid #21262d;
    padding-bottom: 8px;
}
.section-content {
    background: #161b22;
    border: 1px solid #30363d;
    border-radius: 6px;
    padding: 16px;
    margin: 12px 0;
    font-family: 'SF Mono', 'Fira Code', monospace;
    font-size: 0.9em;
    white-space: pre-wrap;
}
.footer {
    margin-top: 40px;
    padding-top: 16px;
    border-top: 1px solid #30363d;
    color: #484f58;
    font-size: 0.85em;
}
.pass { color: #3fb950; }
.fail { color: #f85149; }
.warn { color: #d29922; }
""")
    lines.append("</style>")
    lines.append("</head>")
    lines.append("<body>")
    lines.append("<h1>Loki Mode Session Report</h1>")

    for title, content in sections:
        lines.append(f"<h2>{_html_escape(title)}</h2>")
        lines.append("<div class=\"section-content\">")
        for line in content:
            escaped = _html_escape(line)
            # Highlight pass/fail
            if "PASS" in line:
                escaped = escaped.replace("PASS", "<span class=\"pass\">PASS</span>")
            if "failure" in line.lower() or "FAIL" in line.lower() or "failed" in line.lower():
                escaped = escaped.replace("failure", "<span class=\"fail\">failure</span>")
                escaped = escaped.replace("FAIL", "<span class=\"fail\">FAIL</span>")
                escaped = escaped.replace("failed", "<span class=\"fail\">failed</span>")
                escaped = escaped.replace("Failed", "<span class=\"fail\">Failed</span>")
            lines.append(escaped)
        lines.append("</div>")

    lines.append("<div class=\"footer\">")
    ts = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
    lines.append(f"Generated: {ts} | Loki Mode v{_html_escape(version or 'unknown')}")
    lines.append("</div>")
    lines.append("</body>")
    lines.append("</html>")
    return "\n".join(lines)

def _html_escape(s):
    return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")

if fmt == "text":
    print(render_text(sections))
elif fmt == "markdown":
    print(render_markdown(sections))
elif fmt == "html":
    print(render_html(sections))
REPORT_SCRIPT
    ) || {
        echo -e "${RED}Report generation failed${NC}"
        exit 1
    }

    if [ -n "$output_file" ]; then
        echo "$report_output" > "$output_file"
        echo -e "${GREEN}Report saved to: $output_file${NC}"
    else
        echo "$report_output"
    fi
}

cmd_share() {
    local format="markdown"
    # Secret by default: --public is passed to gh only on an explicit --public.
    local visibility=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                echo -e "${BOLD}loki share${NC} - Share session report as a GitHub Gist"
                echo ""
                echo "Usage: loki share [options]"
                echo ""
                echo "Generates a session report and uploads it as a GitHub Gist,"
                echo "returning a shareable URL."
                echo ""
                echo "Options:"
                echo "  --public             Create a PUBLIC gist, visible to anyone with the URL"
                echo "  --private            Create a secret gist (default: secret; accepted for compatibility)"
                echo "  --format <fmt>       Report format: text, markdown, html (default: markdown)"
                echo "  --help, -h           Show this help"
                echo ""
                echo "Examples:"
                echo "  loki share                         # Secret markdown gist (default)"
                echo "  loki share --public                # Public gist"
                echo "  loki share --format html           # Secret HTML gist"
                echo "  loki share --public --format text  # Public text gist"
                echo ""
                echo "Requires: gh CLI (https://cli.github.com)"
                exit 0
                ;;
            --private) visibility=""; shift ;;
            --public) visibility="--public"; shift ;;
            --format) format="${2:-markdown}"; shift 2 ;;
            --format=*) format="${1#*=}"; shift ;;
            *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki share --help' for usage." >&2; exit 1 ;;
        esac
    done

    # Validate format
    case "$format" in
        text|markdown|md|html) ;;
        *) echo -e "${RED}Unknown format: $format${NC}"; echo "Supported: text, markdown, html"; exit 1 ;;
    esac
    [ "$format" = "md" ] && format="markdown"

    # Check gh CLI is installed
    if ! command -v gh &>/dev/null; then
        echo -e "${RED}gh CLI not found${NC}"
        echo ""
        echo "Install the GitHub CLI to use 'loki share':"
        echo "  brew install gh        # macOS"
        echo "  sudo apt install gh    # Ubuntu/Debian"
        echo "  https://cli.github.com # Other platforms"
        exit 1
    fi

    # Check gh is authenticated
    if ! gh auth status &>/dev/null 2>&1; then
        echo -e "${RED}GitHub CLI not authenticated${NC}"
        echo ""
        echo "Run 'gh auth login' to authenticate, then try again."
        exit 1
    fi

    # Check .loki directory exists
    local loki_dir="${LOKI_DIR:-.loki}"
    if [ ! -d "$loki_dir" ]; then
        echo -e "${RED}No .loki/ directory found${NC}"
        echo "Run 'loki start' first to create a session."
        exit 1
    fi

    # Determine file extension for gist
    local ext="md"
    case "$format" in
        text) ext="txt" ;;
        markdown) ext="md" ;;
        html) ext="html" ;;
    esac

    # Generate report to temp file
    local tmpdir tmpfile
    tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/loki-share.XXXXXXXX") || exit 1
    tmpfile="$tmpdir/session-report.$ext"

    echo "Generating session report..."
    if ! "$0" report --format "$format" > "$tmpfile" 2>/dev/null; then
        echo -e "${RED}Failed to generate session report${NC}"
        rm -f "$tmpfile"; rmdir "$tmpdir" 2>/dev/null || true
        exit 1
    fi

    # Verify report is not empty
    if [ ! -s "$tmpfile" ]; then
        echo -e "${RED}Generated report is empty${NC}"
        rm -f "$tmpfile"; rmdir "$tmpdir" 2>/dev/null || true
        exit 1
    fi

    # Upload as gist
    echo "Uploading session report..."
    local gist_desc="Loki Mode session report ($(date +%Y-%m-%dT%H:%M:%S))"
    _loki_gist_upload "$tmpfile" "$gist_desc" "$visibility"
}

# Internal helper: upload a single file as a GitHub Gist.
# Args: $1=file path, $2=description, $3=visibility ("--public" for a public
# gist, or "" for gh's default, which is a secret gist). NOTE: $3 is intentionally left unquoted at the
# gh call site so an empty value collapses (the --private case sets it to "").
# Removes the input file after the attempt. Prints "Shared: <url>" on success;
# on failure prints the gh error and exits 1.
_loki_gist_upload() {
    local file="$1"
    local desc="$2"
    local visibility="$3"

    # gh prints progress ("- Creating gist...", "Created public gist...") on
    # STDERR and the final gist URL on STDOUT. Capture them separately so the
    # URL we expose and print is clean: folding stderr into stdout pollutes the
    # "Shared:" line and the ready-to-post hook with multi-line gh chatter.
    local gist_out gist_err gist_exit
    local errfile
    if [ "$visibility" = "--public" ]; then
        echo -e "${YELLOW}Warning: this gist is PUBLIC and visible to anyone with the URL.${NC}" >&2
    fi
    # Fall back to /dev/null if mktemp fails (disk full / unwritable /tmp) so the
    # redirect target is always a real path -- never an empty string (2>"" is a
    # bash error). We lose the captured stderr text in that rare case, not the
    # clean URL parse.
    errfile=$(mktemp "/tmp/loki-gist-err-XXXXXX" 2>/dev/null) || errfile=/dev/null
    gist_out=$(gh gist create "$file" --desc "$desc" $visibility 2>"$errfile")
    gist_exit=$?
    gist_err=$(cat "$errfile" 2>/dev/null)
    [ "$errfile" != "/dev/null" ] && rm -f "$errfile"

    # Cleanup temp file and its private loki-* directory (rmdir only removes
    # an empty directory).
    rm -f "$file"
    case "$(basename "$(dirname "$file")")" in
        loki-*) rmdir "$(dirname "$file")" 2>/dev/null || true ;;
    esac

    if [ $gist_exit -ne 0 ]; then
        echo -e "${RED}Failed to create gist${NC}"
        [ -n "$gist_err" ] && echo "$gist_err"
        [ -n "$gist_out" ] && echo "$gist_out"
        exit 1
    fi

    # The URL is the gist permalink on stdout. Extract just the URL line in
    # case any non-URL text slips into stdout on some gh versions.
    local gist_url
    gist_url=$(printf '%s\n' "$gist_out" | grep -Eo 'https://gist\.github\.com/[^[:space:]]+' | head -1)
    [ -z "$gist_url" ] && gist_url="$gist_out"

    # Expose the URL to callers (e.g. cmd_proof prints a ready-to-post hook
    # after this returns). The shared "Shared:" line stays unchanged.
    LOKI_LAST_GIST_URL="$gist_url"
    echo -e "${GREEN}Shared: ${gist_url}${NC}"
}

# loki_tier_gate - R9 open-core tier/license seam.
#
# OSS-FIRST CONTRACT: this is a no-op ALLOW for OSS users. LOKI_TIER defaults
# to "oss" and every existing free feature stays fully free. This function is
# the single place where a future hosted/enterprise build would gate a
# hosted-only capability. It is NEVER called from any existing free command
# path; its only caller is the opt-in --hosted publish seam below. For OSS
# (the default), it always returns 0 (allow).
#
# Args: $1 = capability name (informational; e.g. "hosted_publish").
# Returns: 0 = allowed, 1 = gated (non-OSS tier without entitlement).
# Env:  LOKI_TIER (default "oss"), LOKI_LICENSE_KEY (optional, non-OSS only).
loki_tier_gate() {
    local capability="${1:-}"
    local tier="${LOKI_TIER:-oss}"

    # OSS tier: everything is allowed, always. No license, no network, no gate.
    if [ "$tier" = "oss" ]; then
        return 0
    fi

    # Non-OSS tiers (hosted/enterprise) are a SEAM only. The hosted backend
    # and license-verification service do not exist yet, so we cannot validate
    # an entitlement. Be honest: do not pretend to grant a paid capability.
    # A real hosted build replaces this branch with a verified license check.
    if [ -z "${LOKI_LICENSE_KEY:-}" ]; then
        echo -e "${YELLOW}LOKI_TIER='${tier}' requested but no LOKI_LICENSE_KEY set.${NC}" >&2
        echo "Hosted/enterprise license verification is not available yet." >&2
        echo "OSS users: leave LOKI_TIER unset (or 'oss') -- everything stays free." >&2
        return 1
    fi

    # A license key is present but there is no verification backend yet. We do
    # NOT fabricate a successful verification. The capability stays ungated for
    # OSS-equivalent use; the seam is documented in docs/OPEN-CORE-BOUNDARY.md.
    echo -e "${YELLOW}LOKI_LICENSE_KEY set but the verification backend is not available yet (R9 seam).${NC}" >&2
    return 0
}

# _loki_hosted_publish_proof - R9 hosted proof-publish client stub.
#
# Posts an ALREADY-REDACTED proof page to a self-hosted/SaaS endpoint given by
# LOKI_HOSTED_ENDPOINT. There is NO official Loki hosted backend yet; this is a
# clean client seam an operator can point at their own endpoint. We never
# fabricate a hosted URL: on success we print the URL the endpoint returned (or
# the endpoint itself); on any failure we print an honest error and exit non-0.
#
# Args: $1 = proof id, $2 = redacted index.html path, $3 = proof.json path.
# Returns: 0 on success, non-zero on missing endpoint / transport / non-2xx.
_loki_hosted_publish_proof() {
    local id="$1"
    local html="$2"
    local pj="$3"

    # Tier seam (no-op allow for OSS). Hosted publish is opt-in regardless.
    loki_tier_gate "hosted_publish" || true

    local endpoint="${LOKI_HOSTED_ENDPOINT:-}"
    if [ -z "$endpoint" ]; then
        echo -e "${YELLOW}Hosted publishing backend not available.${NC}" >&2
        echo "There is no official Loki hosted service yet (R9 ships the seam, not a live backend)." >&2
        echo "To publish to your own hosted endpoint, set LOKI_HOSTED_ENDPOINT to its URL." >&2
        echo "Or publish to a GitHub Gist instead: loki proof share ${id}" >&2
        return 1
    fi

    if ! command -v curl &>/dev/null; then
        echo -e "${RED}curl not found${NC}" >&2
        echo "Hosted publishing requires curl. Install curl or use: loki proof share ${id}" >&2
        return 1
    fi

    # CREDIBILITY: we upload the file the generator already redacted (the same
    # bytes 'loki proof share' would put on a gist). We do not build a fresh
    # body that could bypass redaction. If proof.json reports redaction was not
    # applied, refuse -- never publish an unredacted artifact.
    if [ -f "$pj" ]; then
        local redaction_ok
        redaction_ok=$(LOKI_PROOF_JSON="$pj" python3 - <<'PYEOF' 2>/dev/null || echo "unknown"
import json, os
try:
    d = json.load(open(os.environ["LOKI_PROOF_JSON"]))
except Exception:
    print("unknown")
else:
    print("yes" if (d.get("redaction") or {}).get("applied") else "no")
PYEOF
)
        if [ "$redaction_ok" = "no" ]; then
            echo -e "${RED}Refusing to publish: proof redaction was not applied.${NC}" >&2
            echo "Regenerate the proof (LOKI_PROOF=1) so the redactor runs, then retry." >&2
            return 1
        fi
    fi

    echo -e "${BOLD}Publishing proof '${id}' to hosted endpoint${NC}"
    echo "  endpoint: ${endpoint}"
    echo "  payload:  ${html} (already redacted by the generator)"
    echo ""

    # POST the redacted HTML. Auth header is sent only if a license key exists;
    # OSS users with their own endpoint need no key.
    local tmp_body tmp_code tmp_body_dir
    tmp_body_dir=$(mktemp -d "${TMPDIR:-/tmp}/loki-hosted.XXXXXXXX") || return 1
    tmp_body="$tmp_body_dir/response.out"
    local -a curl_args=(-sS -o "$tmp_body" -w '%{http_code}' -X POST
        -H "Content-Type: text/html"
        -H "X-Loki-Proof-Id: ${id}"
        --data-binary "@${html}")
    if [ -n "${LOKI_LICENSE_KEY:-}" ]; then
        curl_args+=(-H "Authorization: Bearer ${LOKI_LICENSE_KEY}")
    fi
    tmp_code=$(curl "${curl_args[@]}" "$endpoint" 2>/dev/null)
    local curl_exit=$?

    if [ "$curl_exit" -ne 0 ]; then
        echo -e "${RED}Failed to reach hosted endpoint (curl exit ${curl_exit}).${NC}" >&2
        echo "Check LOKI_HOSTED_ENDPOINT or publish to a gist: loki proof share ${id}" >&2
        rm -f "$tmp_body"; rmdir "$tmp_body_dir" 2>/dev/null || true
        return 1
    fi

    # Accept any 2xx. The published URL comes from the endpoint response if it
    # returns one (we look for a "url" field), else we report the endpoint. We
    # NEVER print a fabricated URL.
    case "$tmp_code" in
        2*)
            local published_url
            published_url=$(LOKI_HOSTED_BODY="$tmp_body" LOKI_HOSTED_EP="$endpoint" python3 - <<'PYEOF' 2>/dev/null || true
import json, os
body_path = os.environ["LOKI_HOSTED_BODY"]
try:
    txt = open(body_path).read().strip()
except Exception:
    txt = ""
url = ""
try:
    d = json.loads(txt)
    if isinstance(d, dict):
        url = d.get("url") or d.get("public_url") or ""
except Exception:
    url = ""
print(url)
PYEOF
)
            rm -f "$tmp_body"; rmdir "$tmp_body_dir" 2>/dev/null || true
            if [ -n "$published_url" ]; then
                echo -e "${GREEN}Published: ${published_url}${NC}"
            else
                echo -e "${GREEN}Published to ${endpoint} (HTTP ${tmp_code}).${NC}"
                echo "The endpoint did not return a 'url' field; check your endpoint's response."
            fi
            return 0
            ;;
        *)
            echo -e "${RED}Hosted endpoint returned HTTP ${tmp_code}.${NC}" >&2
            if [ -s "$tmp_body" ]; then
                echo "Response:" >&2
                head -c 500 "$tmp_body" >&2
                echo "" >&2
            fi
            echo "Nothing was published. Or publish to a gist: loki proof share ${id}" >&2
            rm -f "$tmp_body"; rmdir "$tmp_body_dir" 2>/dev/null || true
            return 1
            ;;
    esac
}

# loki bench - head-to-head benchmark harness (R2).
# Subcommands: run <task> | vs <task> | list | verify <result.json> | attribution.
# Thin pass-through to benchmarks/bench/run.sh (shared python core runner.py).
# CREDIBILITY: Loki NEVER grades its own success. Success is decided by a
# held-out acceptance command run by the grader OUTSIDE the agent (exit code).
# No council / RARV-C / LLM-judge participates in scoring. See
# benchmarks/SCHEMA-adapter.md and benchmarks/SCHEMA-result.md.
cmd_bench() {
    local bench_sh="$SKILL_DIR/benchmarks/bench/run.sh"
    if [ ! -f "$bench_sh" ]; then
        # The benchmarks/ harness (~8MB, mostly historical result dumps) is a
        # dev/research feature deliberately omitted from the published package
        # (.npmignore excludes benchmarks/). On a packaged install it is absent,
        # so degrade honestly instead of a bare "not found" that reads like a
        # broken install. Run it from a loki-mode source checkout.
        echo "loki bench: the benchmark harness is not available in this install." >&2
        echo "It is a development/research feature shipped only in the loki-mode source" >&2
        echo "repository, not the published package. Clone the repo and run:" >&2
        echo "  ./benchmarks/run-benchmarks.sh humaneval --execute --loki" >&2
        return 0
    fi
    local sub="${1:-}"
    case "$sub" in
        ""|--help|-h|help)
            echo -e "${BOLD}loki bench${NC} - head-to-head benchmark harness (R2)"
            echo ""
            echo "Usage: loki bench <subcommand> [args] [options]"
            echo ""
            echo "Subcommands:"
            echo "  run <task>      Run Loki only on a task-spec"
            echo "  vs <task>       Run all configured tools on a task-spec (head-to-head)"
            echo "  list            List available task-specs"
            echo "  verify <file>   Recompute task_hash + check tool versions for a result.json"
            echo "  oracles         Are the graders trustworthy? Hash-bound, no spend, no provider"
            echo "  report <files>  Build results.json + RESULTS.md from per-tool result-rows"
            echo "  attribution     Same-model harness attribution: raw Claude Code vs Loki (--dry-run, --execute)"
            echo ""
            echo "Options:"
            echo "  --trials N      Number of trials per tool (default 3)"
            echo "  --model NAME    Override the model"
            echo "  --effort LIST   Comma list of effort levels (low,medium,high,xhigh,max); one result per level"
            echo "  --emit-proof    Emit an R1 proof-of-run for the Loki trial(s)"
            echo "  --out-dir DIR   (report) output dir for results.json + RESULTS.md"
            echo ""
            echo "Loki NEVER grades its own success: a held-out acceptance command run"
            echo "by the grader OUTSIDE the agent decides success by exit code."
            [ "$sub" = "" ] && return 1
            return 0
            ;;
    esac
    bash "$bench_sh" "$@"
}

# loki own - finish-and-own (v7.88.0): a plain-English ownership handoff for a
# NON-technical owner. A pure render (autonomy/lib/own-render.py) over the data
# Loki already captured -- the Evidence Receipt, the completion summary, and
# USAGE.md -- so it cannot fabricate: the "is it working?" verdict comes verbatim
# from the receipt's honest headline and is never green unless the receipt is
# VERIFIED. Default prints the doc; --md writes HANDOFF.md; --json for tooling.
# `loki handoff` is an alias.
cmd_own() {
    local renderer="${_LOKI_SCRIPT_DIR}/lib/own-render.py"
    if [ ! -f "$renderer" ]; then
        echo -e "${RED}Finish-and-own renderer not found (autonomy/lib/own-render.py).${NC}" >&2
        exit 2
    fi
    case "${1:-}" in
        --help|-h|help)
            echo -e "${BOLD}loki own${NC} - a plain-English ownership handoff (alias: loki handoff)"
            echo ""
            echo "Usage: loki own [--md | --json]"
            echo ""
            echo "Explains, in plain language for a non-technical owner: what was"
            echo "built, whether Loki verified it works, how to run it, how to put"
            echo "it online, what a developer needs to know, and what is left to do."
            echo "It reads the last build's Evidence Receipt + completion summary;"
            echo "the 'is it working' verdict is the receipt's honest headline (never"
            echo "green unless the build is VERIFIED)."
            echo ""
            echo "Options:"
            echo "  --md     Write HANDOFF.md to the project root"
            echo "  --json   Emit the structured handoff as JSON"
            exit 0
            ;;
    esac
    # --md writes HANDOFF.md at the project root (matches the help). The renderer
    # prints markdown on stdout; the CLI places it as a file, atomically (temp+mv)
    # so a partial write never leaves a truncated HANDOFF.md. Any other args
    # (--json, default) pass through and print.
    if [ "${1:-}" = "--md" ]; then
        local _handoff="${TARGET_DIR:-.}/HANDOFF.md"
        local _handoff_tmp="${TARGET_DIR:-.}/.HANDOFF.md.tmp"
        if python3 "$renderer" --loki-dir "${LOKI_DIR:-.loki}" --md > "$_handoff_tmp" 2>/dev/null; then
            mv -f "$_handoff_tmp" "$_handoff" && \
                echo -e "${GREEN}Wrote ${_handoff}${NC} - open it or hand it to whoever owns this build." || \
                { rm -f "$_handoff_tmp" 2>/dev/null; echo -e "${RED}Could not write HANDOFF.md${NC}" >&2; exit 1; }
        else
            rm -f "$_handoff_tmp" 2>/dev/null
            echo -e "${RED}Could not render the ownership handoff${NC}" >&2
            exit 1
        fi
        exit 0
    fi
    python3 "$renderer" --loki-dir "${LOKI_DIR:-.loki}" "$@"
    exit $?
}

# loki secure - the secure-by-default gate surface (v7.87.0).
# Subcommands: list (show findings) | waive <rule> <file> [reason] | unwaive.
# Waivers are written to .loki/quality/security-waivers.json, which the gate
# (run.sh run_secure_scan) and the Evidence Receipt both READ + honor. The gate
# is advisory by default; LOKI_SECURE_GATE=block makes un-waived HIGH findings
# block. Honest: a waiver is RECORDED in the receipt (accepted with intent), never
# silently hides a finding.
cmd_secure() {
    local loki_dir="${LOKI_DIR:-.loki}"
    local quality_dir="${loki_dir}/quality"
    local findings_file="${quality_dir}/security-findings.json"
    local waivers_file="${quality_dir}/security-waivers.json"
    local sub="${1:-}"
    [ $# -gt 0 ] && shift
    case "$sub" in
        ""|--help|-h|help)
            echo -e "${BOLD}loki secure${NC} - secure-by-default gate: findings + waivers"
            echo ""
            echo "Usage: loki secure <subcommand> [args]"
            echo ""
            echo "Subcommands:"
            echo "  list                 Show security findings from the last scan"
            echo "  waive <rule> <file> [reason]   Waive a finding (accepted with intent)"
            echo "  unwaive <rule> <file>          Remove a waiver"
            echo ""
            echo "The gate is advisory by default; set LOKI_SECURE_GATE=block to make"
            echo "un-waived HIGH findings block completion. Waivers are recorded in the"
            echo "Evidence Receipt (loki proof show) -- they are never hidden."
            [ "$sub" = "" ] && exit 1
            exit 0
            ;;
        list)
            if [ ! -f "$findings_file" ]; then
                echo -e "${YELLOW}No security scan results yet.${NC} Run 'loki start' (the gate runs in the review phase)."
                exit 0
            fi
            if command -v jq &>/dev/null; then
                jq '.findings' "$findings_file" 2>/dev/null || cat "$findings_file"
            else
                LOKI_SEC_F="$findings_file" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_SEC_F'])); [print('%s [%s] %s%s -- %s' % (f.get('severity','?'), f.get('rule','?'), f.get('file','?'), (':'+str(f['line'])) if f.get('line') else '', f.get('fix',''))) for f in d.get('findings',[])] or print('No findings.')"
            fi
            exit 0
            ;;
        waive)
            local rule="${1:-}" file="${2:-}" reason="${3:-waived via loki secure}"
            if [ -z "$rule" ] || [ -z "$file" ]; then
                echo -e "${RED}Usage: loki secure waive <rule> <file> [reason]${NC}" >&2
                exit 2
            fi
            mkdir -p "$quality_dir"
            LOKI_SEC_W="$waivers_file" LOKI_SEC_RULE="$rule" LOKI_SEC_FILE="$file" LOKI_SEC_REASON="$reason" python3 - <<'PYW'
import json, os
p = os.environ["LOKI_SEC_W"]
try:
    with open(p) as f: data = json.load(f)
    if not isinstance(data, dict): data = {}
except Exception:
    data = {}
waivers = data.get("waivers")
if not isinstance(waivers, list): waivers = []
rule, fl, reason = os.environ["LOKI_SEC_RULE"], os.environ["LOKI_SEC_FILE"], os.environ["LOKI_SEC_REASON"]
if not any(w.get("rule") == rule and w.get("file") == fl for w in waivers if isinstance(w, dict)):
    waivers.append({"rule": rule, "file": fl, "reason": reason})
data["waivers"] = waivers
tmp = p + ".tmp"
with open(tmp, "w") as f: json.dump(data, f, indent=2)
os.replace(tmp, p)
print("Waived %s on %s (recorded in the Evidence Receipt)." % (rule, fl))
PYW
            exit $?
            ;;
        unwaive)
            local rule="${1:-}" file="${2:-}"
            if [ -z "$rule" ] || [ -z "$file" ]; then
                echo -e "${RED}Usage: loki secure unwaive <rule> <file>${NC}" >&2
                exit 2
            fi
            [ -f "$waivers_file" ] || { echo "No waivers to remove."; exit 0; }
            LOKI_SEC_W="$waivers_file" LOKI_SEC_RULE="$rule" LOKI_SEC_FILE="$file" python3 - <<'PYU'
import json, os
p = os.environ["LOKI_SEC_W"]
try:
    with open(p) as f: data = json.load(f)
except Exception:
    data = {}
waivers = [w for w in (data.get("waivers") or [])
           if not (isinstance(w, dict) and w.get("rule") == os.environ["LOKI_SEC_RULE"]
                   and w.get("file") == os.environ["LOKI_SEC_FILE"])]
data["waivers"] = waivers
tmp = p + ".tmp"
with open(tmp, "w") as f: json.dump(data, f, indent=2)
os.replace(tmp, p)
print("Removed waiver for %s on %s." % (os.environ["LOKI_SEC_RULE"], os.environ["LOKI_SEC_FILE"]))
PYU
            exit $?
            ;;
        *)
            echo -e "${RED}Unknown subcommand: secure $sub${NC}" >&2
            echo "Try: loki secure --help"
            exit 2
            ;;
    esac
}

# loki proof - inspect and share proof-of-run artifacts (.loki/proofs/<id>/).
# Subcommands: list | show <id> | open <id> | share <id>.
# The proof.json schema is frozen (R1 spec). Reads are tolerant of missing
# keys (early/degraded proofs) and default to "-".
# True when <id> is a safe 'loki start' run id with a sealed receipt under <loki_dir>/runs.
_proof_is_run_id() {
    case "$1" in
        ''|*..*|[!A-Za-z0-9]*|*[!A-Za-z0-9._-]*) return 1 ;;
    esac
    [ -f "$2/runs/$1/receipt.json" ]
}

cmd_proof() {
    local loki_dir="${LOKI_DIR:-.loki}"
    local proofs_dir="${loki_dir}/proofs"
    local sub="${1:-}"
    [ $# -gt 0 ] && shift

    case "$sub" in
        ""|--help|-h|help)
            echo -e "${BOLD}loki proof${NC} - inspect and share proof-of-run artifacts"
            echo ""
            echo "Usage: loki proof <subcommand> [args]"
            echo ""
            echo "Subcommands:"
            echo "  list                 List proof-of-run artifacts in .loki/proofs/"
            echo "  releases [--json]    Release history from git tags; version_is_ahead"
            echo "                       reads UNKNOWN when it cannot be computed"
            echo "  phases [--json]      Measured phase history from real phase_change"
            echo "                       events; unrecorded boundaries read UNKNOWN"
            echo "  show <id> [--json]   Compact reviewer summary (label, revision, checks,"
            echo "                       requirements, modified tests); --json prints the"
            echo "                       full .loki/proofs/<id>/proof.json"
            echo "  verify <id>          Re-check a receipt against the repo (tamper + drift)."
            echo "                       Exit 0 clean, 1 tamper/drift, 2 could not check,"
            echo "                       64 usage (no id, more than one id, unknown option,"
            echo "                       -h/--help: verify never exits 0 without a verdict),"
            echo "                       66 unknown id. Verify it yourself."
            echo "  passport <contract> <id|proof.json> <output.json>"
            echo "                       Bind work from any agent to a portable Outcome Contract"
            echo "                       and independently rechecked Evidence Receipt"
            echo ""
            echo "Options for 'verify':"
            echo "  --human              Render the verdict and every failure reason as prose"
            echo "  --jwks <url|file>    Also check the receipt's attestation against a published"
            echo "                       key set. Accepts a server URL or a local jwks.json, so a"
            echo "                       third party can check WHO produced a receipt with no API"
            echo "                       token, no gpg key import, and no network. Exit 1 if the"
            echo "                       token FAILED or the receipt is unsigned (ABSENT); 2 if"
            echo "                       NOT CHECKED (key set unreadable or verifier dependency"
            echo "                       missing); 64 if --jwks has no value or an empty one."
            echo "                       A 1 is never softened to 2."
            echo ""
            echo "More subcommands:"
            echo "  md <id>              Paste-able Markdown for a PR comment or Slack"
            echo "  open <id>            Open .loki/proofs/<id>/index.html in a browser"
            echo "  share <id>           Publish the proof page as a GitHub Gist (opt-in)"
            echo "  bundle <run-id>      Write a portable receipt bundle for offline verification"
            echo "                       [--out <dir>] (default ./loki-bundle-<run-id>; Bun route)"
            echo ""
            echo "Options for 'share':"
            echo "  --yes                Skip the redaction-preview confirmation prompt"
            echo "  --public             Create a PUBLIC gist, visible to anyone with the URL"
            echo "  --private            Create a secret gist (default: secret; accepted for compatibility)"
            echo "  --hosted             Publish to LOKI_HOSTED_ENDPOINT (open-core seam; no official backend yet)"
            echo ""
            echo "Proofs are generated automatically at run completion (LOKI_PROOF=0 to opt out)."
            echo ""
            echo "Signing: receipts are Ed25519-signed automatically (key auto-generated at"
            echo "~/.loki/keys/receipt-ed25519.pem). To use your own key set"
            echo "LOKI_RECEIPT_SIGNING_KEY or LOKI_RECEIPT_SIGNING_KEY_FILE."
            echo "See docs/SIGNED-RECEIPTS.md."
            [ "$sub" = "" ] && exit 1
            exit 0
            ;;
        releases)
            # CLI PARITY with GET /api/operator/releases. Same reader
            # (dashboard/api_releases.py), so the two surfaces cannot disagree
            # about which versions shipped.
            _rel_json=0
            [ "${1:-}" = "--json" ] && _rel_json=1
            python3 - "$_rel_json" "$SKILL_DIR" <<'PYREL'
import json, os, sys
sys.dont_write_bytecode = True
# Resolve the repo from THIS SCRIPT's location, not the cwd. Using getcwd()
# made `loki proof releases` fail with "No module named dashboard" whenever it
# was run from any other directory -- and it exited 0 while doing so, which is
# a failure reported as success.
# SKILL_DIR is the CLI's own install root, computed at loki:216. sys.argv[0]
# is "-" inside a heredoc, so deriving the path from it does not work.
_repo = sys.argv[2] if len(sys.argv) > 2 else ""
for _cand in (_repo, os.getcwd()):
    if os.path.isdir(os.path.join(_cand, "dashboard")):
        sys.path.insert(0, _cand)
        break
as_json = sys.argv[1] == "1"
try:
    from dashboard import api_releases
except Exception as exc:
    sys.stderr.write("release history unavailable: %s\n" % exc)
    raise SystemExit(2)
env = api_releases.list_releases(os.getcwd())
if as_json:
    print(json.dumps(env, indent=2))
    raise SystemExit(0)
rows = env.get("releases") or []
if not rows:
    print("no releases readable: %s" % (env.get("reason") or "unknown"))
    raise SystemExit(3)
# version_is_ahead is None when it could not be computed. None is NOT False:
# "I cannot compare" is a different claim from "it is not ahead".
ahead = env.get("version_is_ahead")
print("VERSION %s | newest tag %s | ahead: %s" % (
    env.get("version") or "UNKNOWN",
    env.get("newest_tag") or "UNKNOWN",
    "UNKNOWN" if ahead is None else ("yes" if ahead else "no")))
for r in rows:
    print("  %-14s %s%s" % (r.get("tag"), r.get("date") or "date UNKNOWN",
                            "  (current)" if r.get("is_current") else ""))
PYREL
            return $?
            ;;
        phases)
            # CLI PARITY with GET /api/operator/phases. Both call the SAME
            # reader (dashboard/api_phases.py), so the two surfaces cannot
            # drift into disagreeing about the same run.
            _phases_json=0
            [ "${1:-}" = "--json" ] && _phases_json=1
            python3 - "$loki_dir" "$_phases_json" <<'PYPHASES'
import json, os, sys
sys.dont_write_bytecode = True
sys.path.insert(0, os.getcwd())
loki_dir, as_json = sys.argv[1], sys.argv[2] == "1"
try:
    from dashboard import api_phases
except Exception as exc:
    sys.stderr.write("phase history unavailable: %s\n" % exc)
    raise SystemExit(2)
env = api_phases.phase_history(loki_dir)
if as_json:
    print(json.dumps(env, indent=2))
    raise SystemExit(0)
segs = env.get("segments") or []
if not segs:
    # An empty result states WHY: "no phases" and "could not read" are
    # different operator situations and must not print identically.
    print("no measured phase history: %s" % (env.get("reason") or "unknown"))
    raise SystemExit(3)
lead = env.get("leading_phase")
rows = ([lead] if lead else []) + list(segs)
for seg in rows:
    print("  %-12s start=%-14s end=%s" % (
        seg.get("phase"),
        seg.get("start") if seg.get("start") is not None else "UNKNOWN",
        seg.get("end") if seg.get("end") is not None else "UNKNOWN"))
PYPHASES
            return $?
            ;;
        list)
            if [ ! -d "$proofs_dir" ]; then
                echo -e "${YELLOW}No proofs found.${NC} Run 'loki start' to generate one."
                exit 0
            fi
            local found=0
            local pj
            for pj in "$proofs_dir"/*/proof.json; do
                [ -f "$pj" ] || continue
                # Print the header lazily, only once a valid proof is found, so
                # an empty (or proof-less) proofs dir prints just the
                # "No proofs found" line -- matching the Bun route (proof.ts
                # listProofs, which returns before the header when rows is empty).
                if [ "$found" -eq 0 ]; then
                    printf "%-26s  %-20s  %-18s  %-9s  %s\n" "RUN_ID" "GENERATED_AT" "VERDICT" "COST_USD" "FILES"
                fi
                found=1
                LOKI_PROOF_JSON="$pj" python3 - <<'PYEOF'
import json, os
p = os.environ["LOKI_PROOF_JSON"]
try:
    with open(p) as f:
        d = json.load(f)
except Exception:
    d = {}
# Coerce missing AND explicitly-null fields to "-" so a null final_verdict /
# cost.usd / count renders as "-" rather than Python's str(None) == "None".
# This matches the Bun route (proof.ts str(): null/undefined -> "-").
def s(v):
    return "-" if v is None else str(v)
run_id = d.get("run_id")
gen = d.get("generated_at")
# The honest verdict lives in honesty.headline (VERIFIED / VERIFIED WITH GAPS /
# NOT VERIFIED); fall back to legacy council.final_verdict for older proofs.
verdict = (d.get("honesty") or {}).get("headline")
if verdict is None:
    verdict = (d.get("council") or {}).get("final_verdict")
cost = (d.get("cost") or {}).get("usd")
files = (d.get("files_changed") or {}).get("count")
print("{:<26}  {:<20}  {:<18}  {:<9}  {}".format(
    s(run_id), s(gen), s(verdict), s(cost), s(files)))
PYEOF
            done
            if [ "$found" -eq 0 ]; then
                echo -e "${YELLOW}No proofs found.${NC} Run 'loki start' to generate one."
            fi
            exit 0
            ;;
        show)
            local _ps_json=0 _ps_a _ps_args=()
            for _ps_a in "$@"; do
                case "$_ps_a" in
                    --json) _ps_json=1 ;;
                    *) _ps_args+=("$_ps_a") ;;
                esac
            done
            set -- "${_ps_args[@]+"${_ps_args[@]}"}"
            local id="${1:-}"
            if [ -z "$id" ]; then
                echo -e "${RED}Missing proof id.${NC} Use 'loki proof list'."
                exit 2
            fi
            local pj="${proofs_dir}/${id}/proof.json"
            if [ ! -f "$pj" ] && _proof_is_run_id "$id" "${proofs_dir%/*}"; then
                # A 'loki start' run id: the Bun route answers through the engine10 verifier.
                if [ "$_ps_json" -eq 1 ]; then
                    _loki_bun_delegate proof show "$id" --json
                else
                    _loki_bun_delegate proof show "$id"
                fi
                exit $?
            fi
            if [ ! -f "$pj" ]; then
                echo -e "${RED}Proof not found: ${id}${NC}"
                echo "Use 'loki proof list' to see available proofs."
                exit 1
            fi
            if [ "$_ps_json" -eq 0 ]; then
                # Compact reviewer summary (docs/PROOF-PASSPORT.md).
                local _ps_py _ps_vr="${_LOKI_SCRIPT_DIR}/lib/proof-verify.py"
                _ps_py="$(loki_verify_tool_path python3)" || {
                    echo -e "${RED}python3 is unavailable${NC}" >&2
                    exit 2
                }
                local _ps_root
                _ps_root="$(loki_verify_root 2>/dev/null)" || {
                    echo -e "${YELLOW}Summary unavailable (project tree not resolved; not checking the ambient cwd).${NC} Use 'loki proof show ${id} --json' for the full record." >&2
                    exit 2
                }
                "$_ps_py" -E "$_ps_vr" summary "$pj" --repo "$_ps_root" --loki-dir "$loki_dir" || exit $?
                exit 0
            fi
            if command -v jq &>/dev/null; then
                jq . "$pj"
            else
                # this is a verify-adjacent read of proof.json. -E stops
                # PYTHONPATH/sitecustomize.py from acting before this line, and
                # python3 is resolved to an absolute path first so an empty
                # PATH component cannot let a committed python3 shim in the
                # checkout answer instead of the real interpreter. The cwd is
                # dropped from sys.path before the real json/os import, so a
                # committed json.py/os.py cannot answer either.
                local _show_py3
                _show_py3="$(loki_verify_tool_path python3)" || {
                    echo -e "${RED}python3 is unavailable${NC}" >&2
                    exit 2
                }
                LOKI_PROOF_JSON="$pj" "$_show_py3" -E -c "
import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
import json, os
print(json.dumps(json.load(open(os.environ['LOKI_PROOF_JSON'])), indent=2))
"
            fi
            exit 0
            ;;
        passport)
            # EXEC-PROOF-01: make the provider-neutral Proof Passport reachable
            # from the installed product. The Python implementation already
            # binds the executor/verifier identities, exact contract digest,
            # exact receipt bytes, and independently re-derived verdict. Before
            # this subcommand, users had to know the repository-only tools/
            # path, so the portable proof workflow was not actually a product
            # workflow for npm consumers.
            #
            # The second positional accepts either a local proof id or an
            # explicit receipt path. That lets a Loki run use its short id while
            # Codex, Claude Code, Cursor, Factory, or any other executor can hand
            # over a provider-neutral proof.json without copying it into .loki.
            local contract="${1:-}"
            local receipt_ref="${2:-}"
            local output="${3:-}"
            if [ -z "$contract" ] || [ -z "$receipt_ref" ] || [ -z "$output" ]; then
                echo -e "${RED}Usage: loki proof passport <contract.json> <proof-id|proof.json> <output.json> [options]${NC}" >&2
                echo "Options: --repo-dir DIR --markdown-output FILE --force" >&2
                exit 64
            fi
            shift 3

            local receipt_path="$receipt_ref"
            if [ ! -f "$receipt_path" ]; then
                receipt_path="${proofs_dir}/${receipt_ref}/proof.json"
            fi
            if [ ! -f "$receipt_path" ]; then
                echo -e "${RED}Receipt not found: ${receipt_ref}${NC}" >&2
                echo "Pass a proof id from 'loki proof list' or a path to proof.json." >&2
                exit 2
            fi

            local passport_tool="${SKILL_DIR}/tools/proof-passport.py"
            if [ ! -f "$passport_tool" ]; then
                echo -e "${RED}Proof Passport tool not found: ${passport_tool}${NC}" >&2
                exit 3
            fi
            python3 -E "$passport_tool" "$contract" "$receipt_path" "$output" "$@"
            exit $?
            ;;
        chain)
            # The whole verification chain, one verdict. tools/verify-chain.py
            # shipped in files[] and NO loki command surfaced it, so the command
            # a buyer runs to check our output without trusting us was
            # undiscoverable. That is the wedge, so it gets a front door.
            #
            # The tool's exit contract is preserved EXACTLY:
            #   0 PASSED  1 FAILED  2 UNAVAILABLE  3 NOTHING
            # Collapsing 2 or 3 into 1 would destroy the measured / unreadable /
            # no-records distinction that makes the verdict worth anything: a
            # chain that could not be checked is not a chain that failed.
            local chain_tool="${SKILL_DIR}/tools/verify-chain.py"
            if [ ! -f "$chain_tool" ]; then
                echo -e "${RED}Chain verifier not found: ${chain_tool}${NC}" >&2
                exit 2
            fi
            if ! command -v python3 >/dev/null 2>&1; then
                echo -e "${RED}python3 unavailable: the chain could not be verified (UNAVAILABLE, not a pass)${NC}" >&2
                exit 2
            fi
            # A verifier never exits 0 without a verdict: -h/--help (before any
            # "--") shows usage on stderr and exits 64, where argparse would
            # exit 0 with nothing checked. The tool itself keeps its exit-0
            # --help for direct use (tests/test_tool_exit_contract.py).
            local _ca
            for _ca in "$@"; do
                [ "$_ca" = "--" ] && break
                # argparse also treats --h/--he/--hel and any short-option
                # cluster containing h (-hq) as help.
                case "$_ca" in
                    -h*|--h|--he|--hel|--help|-[!-]*h*)
                        python3 -E "$chain_tool" --help >&2 || true
                        echo "Nothing was checked (exit 64). Full help: loki proof help" >&2
                        exit 64
                        ;;
                esac
            done
            # `set -e` is active (loki:22) and a FAILED verdict is a NORMAL
            # result here, so a bare call would abort before the status is read.
            local _chain_rc=0
            python3 -E "$chain_tool" "$@" || _chain_rc=$?
            exit $_chain_rc
            ;;
        md)
            # Paste-able Markdown for a PR comment, a Slack message, or a
            # ticket. The renderer already existed -- render_evidence_receipt_md
            # in lib/proof-pr.sh -- but was reachable ONLY when Loki opened a PR
            # itself. So the one form of the receipt built for humans to forward
            # could not be produced on demand, which is the built-and-unreachable
            # pattern this project keeps finding.
            #
            # Reuses that renderer rather than adding a second one: two writers
            # of one artifact drift, and the drift surfaces as a PR body and a
            # pasted summary disagreeing about the same run.
            local id="${1:-}"
            if [ -z "$id" ]; then
                echo -e "${RED}Missing proof id.${NC} Use 'loki proof list'." >&2
                exit 2
            fi
            local pj="${proofs_dir}/${id}/proof.json"
            if [ ! -f "$pj" ] && _proof_is_run_id "$id" "${proofs_dir%/*}"; then
                _loki_bun_delegate proof md "$id"
                exit $?
            fi
            if [ ! -f "$pj" ]; then
                echo -e "${RED}Proof not found: ${id}${NC}" >&2
                echo "Use 'loki proof list' to see available proofs." >&2
                exit 1
            fi
            local _pr_lib="$_LOKI_SCRIPT_DIR/lib/proof-pr.sh"
            if [ ! -f "$_pr_lib" ]; then
                echo -e "${RED}Renderer not found: ${_pr_lib}${NC}" >&2
                exit 3
            fi
            # shellcheck source=/dev/null
            source "$_pr_lib"
            render_evidence_receipt_md "$pj"
            exit 0
            ;;
        verify)
            # Deterministic re-check of a receipt against the repo: re-hashes the
            # canonical proof (tamper check) and re-derives the diff from the
            # recorded base_sha vs live HEAD (drift check). Exit 0 = clean, 1 =
            # tamper/drift, 2 = unusable input. This is the "verify it yourself"
            # path: it re-checks integrity + re-derives the diff from git, so an
            # inconsistent edit is caught. It does NOT prove non-forgeability on
            # the unsigned path (a hand-forger can rewrite facts and re-hash);
            # neutral non-forgeability requires the signed record.
            # --human renders the verdict plus every failure reason as prose.
            # JSON stays the DEFAULT so machine consumers, which pipe this
            # stdout verbatim, are unaffected. The flag is STRIPPED from the
            # positional args rather than assumed at a fixed index, so it works
            # before or after the id.
            # --jwks <url-or-file> additionally checks the receipt's
            # attestation against a published key set. This is the third-party
            # path: an auditor who was handed a proof.json can check WHO
            # produced it without running a build, without our API token, and
            # without importing a gpg key. A FILE is accepted as well as a URL
            # so an air-gapped reviewer, handed the receipt and the key set on
            # disk, can still verify -- requiring the network would exclude
            # exactly the reviewer who most needs this.
            # An EMPTY --jwks value ("--jwks ''", "--jwks=") is a usage error,
            # sticky across every occurrence. It used to skip the attestation
            # check and exit 0 on an unsigned receipt, and a later empty
            # --jwks cancelled an earlier real one.
            # An UNKNOWN option, or a second positional, is a usage error too:
            # "--jwk keys.json" and "-jwks keys.json" used to be kept as extra
            # positionals and ignored, so an unsigned receipt exited 0 as if a
            # signature had been checked.
            local _pv_human=0 _pv_jwks="" _pv_jwks_empty=0 _pv_args=()
            local _pv_a _pv_want_jwks=0 _pv_npos=0 _pv_end_opts=0
            for _pv_a in "$@"; do
                # After "--" everything is a proof id, so a caller can pass an
                # id that begins with "-" without it being read as an option.
                if [ "$_pv_end_opts" -eq 1 ]; then
                    _pv_args+=("$_pv_a"); _pv_npos=$((_pv_npos + 1)); continue
                fi
                if [ "$_pv_want_jwks" -eq 1 ]; then
                    _pv_jwks="$_pv_a"; _pv_want_jwks=0
                    [ -n "$_pv_jwks" ] || _pv_jwks_empty=1
                    continue
                fi
                case "$_pv_a" in
                    --human) _pv_human=1 ;;
                    --) _pv_end_opts=1 ;;
                    -h|--help)
                        # verify never exits 0 without a verdict. The documented
                        # call is "loki proof verify <id>", so an id taken from
                        # untrusted content could be "-h"; an exit-0 help would
                        # turn a provenance gate into a pass. Usage goes to
                        # stderr with 64; full exit-0 help is "loki proof help".
                        {
                            echo "Usage: loki proof verify [--human] [--jwks <url|file>] [--] <id>"
                            echo "  Exit 0 clean, 1 tamper/drift (or --jwks FAILED/ABSENT), 2 could not check,"
                            echo "  64 usage (no id, more than one id, unknown option, empty --jwks, -h/--help),"
                            echo "  66 unknown id. '--' ends options, for an id that starts with '-'."
                            echo "  Nothing was checked. Full help: loki proof help"
                        } >&2
                        exit 64  # usage (docs/exit-codes.md)
                        ;;
                    --jwks) _pv_want_jwks=1 ;;
                    --jwks=*)
                        _pv_jwks="${_pv_a#--jwks=}"
                        [ -n "$_pv_jwks" ] || _pv_jwks_empty=1
                        ;;
                    -*)
                        echo -e "${RED}Unknown option for 'loki proof verify': '${_pv_a}'.${NC}" >&2
                        echo "  Valid options: --human, --jwks <url|file>. Nothing was checked." >&2
                        exit 64  # usage (docs/exit-codes.md)
                        ;;
                    *) _pv_args+=("$_pv_a"); _pv_npos=$((_pv_npos + 1)) ;;
                esac
            done
            if [ "$_pv_npos" -gt 1 ]; then
                echo -e "${RED}'loki proof verify' takes one proof id; got ${_pv_npos}: ${_pv_args[*]}${NC}" >&2
                echo "  Nothing was checked." >&2
                exit 64  # usage (docs/exit-codes.md)
            fi
            if [ "$_pv_want_jwks" -eq 1 ]; then
                echo -e "${RED}--jwks needs a URL or file path.${NC}" >&2
                exit 64  # usage (docs/exit-codes.md)
            fi
            if [ "$_pv_jwks_empty" -eq 1 ]; then
                echo -e "${RED}--jwks was given an empty value; pass a URL or file path.${NC}" >&2
                echo "  Nothing was checked: an empty key set must not read as a skipped check." >&2
                exit 64  # usage (docs/exit-codes.md)
            fi
            set -- "${_pv_args[@]+"${_pv_args[@]}"}"

            local id="${1:-}"
            if [ -z "$id" ]; then
                echo -e "${RED}Missing proof id.${NC} Use 'loki proof list'."
                exit 64  # usage (docs/exit-codes.md); 2 means "could not check"
            fi
            local pj="${proofs_dir}/${id}/proof.json"
            if [ ! -f "$pj" ] && _proof_is_run_id "$id" "${proofs_dir%/*}"; then
                # Same verdict and exit codes as `loki verify <run-id>`.
                _loki_bun_delegate engine10 verify "$id"
                exit $?
            fi
            if [ ! -f "$pj" ]; then
                echo -e "${RED}Proof not found: ${id}${NC}"
                echo "Use 'loki proof list' to see available proofs."
                exit 66  # input missing; 1 is reserved for tamper/drift
            fi
            # The verifier ships beside the generator under autonomy/lib/ (same
            # dir resolution run.sh uses for proof-generator.py).
            local verifier="${_LOKI_SCRIPT_DIR}/lib/proof-verify.py"
            if [ ! -f "$verifier" ]; then
                echo -e "${RED}Verifier not found (autonomy/lib/proof-verify.py).${NC}"
                exit 2
            fi
            # `|| _pv_rc=$?` is load-bearing: this script runs under `set -e`
            # (loki:22), so a bare call to a verifier that exits 1 on drift --
            # its NORMAL result -- aborts the whole command before the exit
            # status can be read. Without the guard the attestation block below
            # is unreachable on exactly the receipts it exists to check.
            # No python3 means the verifier cannot run: could not check (2),
            # never a shell's 127 and never a pass.
            if ! command -v python3 >/dev/null 2>&1; then
                echo -e "${YELLOW}NOT CHECKED${NC}: python3 is not on PATH, so the verifier could not run (exit 2)." >&2
                exit 2
            fi
            local _pv_rc=0 _pv_out="" _pv_base_rc=0 _pv_root=""
            _pv_root="$(loki_verify_root)" || {
                echo -e "${YELLOW}NOT CHECKED${NC}: no project tree to verify against (exit 2)." >&2
                exit 2
            }
            # Captured, not printed directly, so that --jwks (below) can patch
            # it: BACKLOG 49 -- when --jwks later downgrades _pv_rc to 1
            # (ABSENT) or 2 (NOT CHECKED), this output's own "ok: true"/
            # VERIFIED header from the base hash/drift check must not still be
            # the last word on stdout. Unflagged (no --jwks), this is printed
            # byte-for-byte as the base verifier produced it, same as the Bun
            # route (proof.ts verifyProof) -- ok already agrees with the exit
            # code there, so there is nothing to patch.
            if [ "$_pv_human" -eq 1 ]; then
                _pv_out="$(python3 -I -S "$verifier" --human "$pj" "$_pv_root")" || _pv_rc=$?
            else
                _pv_out="$(python3 -I -S "$verifier" "$pj" "$_pv_root")" || _pv_rc=$?
            fi
            _pv_base_rc="$_pv_rc"

            # The attestation check runs AFTER, and can only make the verdict
            # worse, never better. A valid attestation does not excuse a failed
            # integrity hash: the signature proves who produced the bytes, not
            # that the bytes are internally consistent. Folding the two together
            # would let a signed-but-tampered receipt exit 0.
            local _pv_attestation_field="n/a"
            if [ -n "$_pv_jwks" ]; then
                local _pv_att
                _pv_att="$(loki_proof_attestation_check "$pj" "$_pv_jwks")"
                case "$_pv_att" in
                    ok)
                        if [ "$(loki_proof_seal_legacy "$pj")" = "legacy" ]; then
                            echo "attestation: VERIFIED against ${_pv_jwks} (metadata not signed (legacy seal.v1): scope, algo and attestation_kid are not covered by the signature)" >&2
                        else
                            echo "attestation: VERIFIED against ${_pv_jwks}" >&2
                        fi
                        echo "  Proven: these bytes were signed by the holder of a key in that set." >&2
                        echo "  NOT proven: that the code is correct. A receipt records which checks ran." >&2
                        _pv_attestation_field="VERIFIED"
                        ;;
                    bad)
                        echo -e "${RED}attestation: FAILED -- the token does not cover these bytes.${NC}" >&2
                        echo "  Either the receipt was altered after signing, or the attestation" >&2
                        echo "  belongs to a different run. Do not trust this receipt." >&2
                        _pv_rc=1
                        _pv_attestation_field="FAILED"
                        ;;
                    none)
                        # Exit 1: the caller supplied a key set and asked who
                        # signed this receipt; it is unsigned, so it is NOT
                        # verified. Exiting 0 here let a stripped signature pass
                        # any CI step that asked for provenance.
                        echo -e "${YELLOW}attestation: ABSENT -- this receipt carries no attestation.${NC}" >&2
                        echo "  Nothing was checked against ${_pv_jwks}. This says only that the" >&2
                        echo "  receipt has no JWKS attestation." >&2
                        echo "  Not verified: a key set was supplied, but the receipt is unsigned (exit 1)." >&2
                        _pv_rc=1
                        _pv_attestation_field="ABSENT"
                        ;;
                    keyset:*)
                        echo -e "${YELLOW}attestation: NOT CHECKED -- ${_pv_att#keyset: }.${NC}" >&2
                        echo "  The key set at ${_pv_jwks} cannot give one usable key for this receipt." >&2
                        echo "  This is not a failure of the receipt. Nothing was proven either way (exit 2)." >&2
                        if [ "$_pv_rc" -eq 0 ]; then _pv_rc=2; fi
                        _pv_attestation_field="NOT CHECKED"
                        ;;
                    *)
                        # Unreachable key set, unreadable file, missing python
                        # deps. NOT a verdict: an absent measurement must never
                        # read as either a pass or an accusation, so it exits 2
                        # (could not check). A tamper/drift 1 from the base
                        # verifier is a finding and is never softened to 2.
                        echo -e "${YELLOW}attestation: NOT CHECKED -- could not check against ${_pv_jwks} (key set unreadable,${NC}" >&2
                        echo -e "${YELLOW}  verifier dependency missing, or the JWKS URL is not https -- only loopback is allowed over plain http).${NC}" >&2
                        echo "  The verifier runs python3 -E, so cryptography supplied through PYTHONPATH is not seen (a site or user-site install is)." >&2
                        echo "  This is not a failure of the receipt. Nothing was proven either way (exit 2)." >&2
                        if [ "$_pv_rc" -eq 0 ]; then _pv_rc=2; fi
                        _pv_attestation_field="NOT CHECKED"
                        ;;
                esac
            fi
            # Only patch when --jwks put a THIRD input (the attestation) into
            # the verdict. With no --jwks, ok/the header already agree with the
            # exit code (both come straight from the base verifier), and
            # output is printed byte-for-byte: unflagged output is identical
            # to before this change, on both the bash and Bun routes.
            if [ -n "$_pv_jwks" ]; then
                # Rewrite the base verifier's output so ok/verdict (JSON) or
                # the header line (--human) always agree with $_pv_rc, the
                # exit code this command actually returns. verdict reads
                # doc["hash_ok"] (the parsed JSON) AND the attestation field
                # together: hash_ok False is TAMPERED, and a bad signature
                # against otherwise-good bytes is ALSO TAMPERED (the token
                # proves the bytes came from an untrusted source, which is the
                # same class of finding as a rewritten hash); diff_drift True
                # (or unverifiable, e.g. wrong TARGET_DIR) is drift, NOT
                # tampering -- loki_remote_verify_receipt draws the same line
                # -- so drift with an otherwise-valid or absent attestation
                # must stay FAILED, never TAMPERED.
                # -E plus the explicit sys.path strip BEFORE importing
                # json/os: PYTHONPATH and a committed sitecustomize.py must
                # not run here, same guard as the attestation check above
                # (loki_proof_attestation_check). The strip runs first so a
                # shadow json.py planted in the invocation cwd cannot
                # intercept this import either.
                _V_BASE_RC="$_pv_base_rc" _V_RC="$_pv_rc" _V_ATTESTATION="$_pv_attestation_field" _V_HUMAN="$_pv_human" python3 -E -c '
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, os

raw = sys.stdin.read()
base_rc = int(os.environ["_V_BASE_RC"])
rc = int(os.environ["_V_RC"])
attestation = os.environ["_V_ATTESTATION"]
human = os.environ["_V_HUMAN"] == "1"

# --human never gets the JSON report (render_reasons only prints a header
# plus prose), so doc stays None there and hash_ok cannot be consulted; the
# JSON branch below parses raw first so it CAN consult doc.get("hash_ok").
doc = None
if not human:
    try:
        doc = json.loads(raw)
    except ValueError:
        doc = None

if rc == 0:
    verdict = "VERIFIED"
elif rc == 2:
    # Exit 2 is always "could not check" (missing verifier, malformed
    # proof.json, or --jwks NOT CHECKED) -- never an accusation.
    verdict = "NOT CHECKED"
elif base_rc != 0:
    # The base check itself found something: a real tamper/drift finding, or
    # it could not run at all (malformed proof.json, doc is None). A definite
    # hash mismatch is TAMPERED. So is a bad signature against otherwise-good
    # bytes (attestation FAILED): the token proves these bytes were not
    # produced by a trusted signer, which is the same accusation a rewritten
    # hash makes, just caught by a different mechanism -- a drifted tree
    # checked against the WRONG key still means "do not trust this receipt,"
    # not merely "could not confirm it." Anything else here (drift, drift-
    # unverifiable, cost/headline incoherence, or a base verifier error) is
    # FAILED, the same word "loki proof verify --human" already uses for a
    # non-tamper finding, rather than an accusation the data does not
    # support. --human never sees doc (it is prose, not JSON), so it relies
    # on the attestation field alone here; the JSON branch below can also
    # consult hash_ok.
    verdict = "TAMPERED" if (attestation == "FAILED" or (doc is not None and doc.get("hash_ok") is False)) else "FAILED"
else:
    # Base check passed (rc would have been 0 otherwise); --jwks is what
    # pushed the final rc to 1. Only two attestation outcomes do that: a bad
    # signature (a tamper finding, once the base bytes are known good) or no
    # signature at all (ABSENT, a fact about the receipt, not an accusation).
    verdict = "TAMPERED" if attestation == "FAILED" else "ABSENT"

if human:
    # The --human vocabulary (render_reasons) is only three words: VERIFIED /
    # COULD NOT CHECK / FAILED. verdict (five words, shared with the JSON
    # branch) maps onto it rather than being printed raw, so a TAMPERED or
    # ABSENT finding still reads as the header word this flag readers
    # already parse for a failure. --human has no fifth word of its own for
    # TAMPERED; a real integrity/attestation failure and a non-tamper finding
    # both print "FAILED" to a human reader, same as before this fix -- only
    # the JSON verdict field distinguishes the two.
    header = {
        "VERIFIED": "VERIFIED",
        "NOT CHECKED": "COULD NOT CHECK",
    }.get(verdict, "FAILED")
    # Only the header line can disagree with rc (it reflects the base check
    # alone, computed before --jwks); replace just that line so the
    # per-reason "  - ..." detail from the base verifier survives untouched.
    lines = raw.split("\n")
    if lines and lines[0].strip() in ("VERIFIED", "COULD NOT CHECK", "FAILED"):
        lines[0] = header
    sys.stdout.write("\n".join(lines))
    sys.exit(0)

if doc is None:
    # Base verifier printed something unparseable (should not happen; the
    # base verifier always emits JSON, even on its own internal error). Pass
    # it through unchanged rather than fabricate a report over data we cannot
    # read.
    sys.stdout.write(raw)
    sys.exit(0)

doc["verdict"] = verdict
doc["attestation"] = attestation
# ok is superseded by verdict, which cannot silently drift from the exit
# code the way a second boolean could. Kept for existing readers, but forced
# to agree with rc instead of repeating the base verifier own possibly-
# passing opinion, so a stdout-only consumer reading "ok" is never told the
# opposite of what this process just exited with.
doc["ok"] = (rc == 0)
print(json.dumps(doc, indent=2))
' <<<"$_pv_out" || printf '%s\n' "$_pv_out"
            else
                printf '%s\n' "$_pv_out"
            fi
            exit "$_pv_rc"
            ;;
        open)
            local id="${1:-}"
            if [ -z "$id" ]; then
                echo -e "${RED}Missing proof id.${NC} Use 'loki proof list'."
                exit 2
            fi
            local html="${proofs_dir}/${id}/index.html"
            if [ ! -f "$html" ]; then
                echo -e "${RED}Proof page not found: ${id}/index.html${NC}"
                echo "Use 'loki proof list' to see available proofs."
                exit 1
            fi
            echo -e "${GREEN}Opening proof: $html${NC}"
            if ! loki_open_url "$html" 2>/dev/null; then
                echo ""
                echo "Browser not opened (headless, test, or no opener)."
                echo "Please open in browser: $html"
            fi
            exit 0
            ;;
        share)
            local id=""
            local skip_confirm=0
            local visibility=""
            local hosted=0
            # every python3 invocation below reads proof.json from inside
            # the checkout being shared. Resolve an absolute path once so an
            # empty PATH component cannot let a committed python3 shim in that
            # checkout answer instead of the real interpreter; each call site
            # also runs -E and drops the cwd from sys.path.
            local _share_py3
            _share_py3="$(loki_verify_tool_path python3)" || _share_py3=""
            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --yes|-y) skip_confirm=1; shift ;;
                    --private) visibility=""; shift ;;
                    --public) visibility="--public"; shift ;;
                    --hosted) hosted=1; shift ;;
                    -*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
                    *) id="$1"; shift ;;
                esac
            done
            if [ -z "$id" ]; then
                echo -e "${RED}Missing proof id.${NC} Use 'loki proof list'."
                exit 2
            fi
            local html="${proofs_dir}/${id}/index.html"
            if [ ! -f "$html" ]; then
                echo -e "${RED}Proof page not found: ${id}/index.html${NC}"
                echo "Use 'loki proof list' to see available proofs."
                exit 1
            fi
            # R9 open-core hosted-publish seam. Only taken when the user
            # explicitly passes --hosted. The default gist path below stays
            # byte-for-byte unchanged for OSS users (zero hosted backend
            # required). We never silent-fall-back to gist here: the user asked
            # for hosted, so we POST to a configured LOKI_HOSTED_ENDPOINT or
            # print an honest "no endpoint configured" message and exit non-zero.
            # We never fabricate a hosted URL.
            if [ "$hosted" -eq 1 ]; then
                _loki_hosted_publish_proof "$id" "$html" "${proofs_dir}/${id}/proof.json"
                exit $?
            fi
            if ! command -v gh &>/dev/null; then
                echo -e "${RED}gh CLI not found${NC}"
                echo "Install the GitHub CLI to publish a proof:"
                echo "  brew install gh        # macOS"
                echo "  sudo apt install gh    # Ubuntu/Debian"
                echo "  https://cli.github.com # Other platforms"
                exit 1
            fi
            if ! gh auth status &>/dev/null 2>&1; then
                echo -e "${RED}GitHub CLI not authenticated${NC}"
                echo "Run 'gh auth login' to authenticate, then try again."
                exit 1
            fi

            # Redaction preview. The generator already redacts the proof
            # before writing index.html, so this is a transparency summary,
            # not a second redaction pass.
            local pj="${proofs_dir}/${id}/proof.json"
            local vis_label="public"
            [ -z "$visibility" ] && vis_label="secret"
            echo -e "${BOLD}Publishing proof '${id}' as a ${vis_label} GitHub Gist${NC}"
            echo ""
            echo "What will be shared:"
            echo "  - ${html}"
            if [ -f "$pj" ] && [ -n "$_share_py3" ]; then
                LOKI_PROOF_JSON="$pj" "$_share_py3" -E - <<'PYEOF' 2>/dev/null || true
# The cwd is the checkout being shared: drop it from sys.path before any
# other import, or a committed json.py/os.py could answer this preview.
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, os
try:
    d = json.load(open(os.environ["LOKI_PROOF_JSON"]))
except Exception:
    d = {}
cost = (d.get("cost") or {}).get("usd", "-")
# usd is null when cost was not collected for this run. Show an honest
# "not recorded" instead of the literal "None" (a credibility wart in the
# preview the sharer reads right before publishing).
if cost is None:
    cost = "not recorded"
files = (d.get("files_changed") or {}).get("count", "-")
verdict = (d.get("council") or {}).get("final_verdict", "-")
red = d.get("redaction") or {}
print("  - cost.usd:        {}".format(cost))
print("  - files_changed:   {}".format(files))
print("  - council verdict: {}".format(verdict))
print("  - redaction:       applied={} rules_version={} redactions_count={}".format(
    red.get("applied", False), red.get("rules_version", "-"), red.get("redactions_count", "-")))
PYEOF
            fi
            echo ""
            echo -e "${YELLOW}Secrets, API keys, tokens, env values, and absolute paths have already been stripped by the generator.${NC}"
            echo ""

            if [ "$skip_confirm" -ne 1 ]; then
                printf "Publish this proof to a %s gist? [y/N] " "$vis_label"
                local reply=""
                read -r reply
                case "$reply" in
                    y|Y|yes|YES|Yes) ;;
                    *) echo "Aborted. Nothing was published."; exit 0 ;;
                esac
            fi

            # Copy to a temp file so _loki_gist_upload can remove it after.
            local tmpfile tmpdir
            tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/loki-proof.XXXXXXXX") || exit 1
            tmpfile="$tmpdir/proof.html"
            cp "$html" "$tmpfile"
            echo "Uploading proof page..."
            local gist_desc="Loki Mode proof-of-run ${id}"
            _loki_gist_upload "$tmpfile" "$gist_desc" "$visibility"

            # Print a ready-to-post one-line hook (real cost + url) so the user
            # can paste it straight to X/HN. When cost was not collected, omit
            # the cost (never fabricate a number, never print "$0.00").
            if [ -n "${LOKI_LAST_GIST_URL:-}" ] && [ -f "$pj" ] && [ -n "$_share_py3" ]; then
                local hook
                hook=$(LOKI_PROOF_JSON="$pj" LOKI_GIST_URL="$LOKI_LAST_GIST_URL" "$_share_py3" -E - <<'PYEOF' 2>/dev/null || true
# The cwd is the checkout being shared: drop it from sys.path before any
# other import, or a committed json.py/os.py could answer this hook text.
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, os
try:
    d = json.load(open(os.environ["LOKI_PROOF_JSON"]))
except Exception:
    d = {}
cost = (d.get("cost") or {})
usd = cost.get("usd")


def fmt_usd(v):
    if v is None:
        return None
    try:
        n = float(v)
    except Exception:
        return None
    s = ("%.4f" % n).rstrip("0").rstrip(".")
    if "." not in s:
        s += ".00"
    elif len(s.split(".")[1]) == 1:
        s += "0"
    return "$" + s


def council_ratio(d):
    c = d.get("council") or {}
    if not c.get("enabled"):
        return None
    revs = c.get("reviewers") or []
    if not isinstance(revs, list) or not revs:
        return None
    ok = sum(1 for r in revs if isinstance(r, dict)
             and str(r.get("vote") or "").upper() in ("APPROVE", "APPROVED"))
    return ok, len(revs)


u = fmt_usd(usd)
lead = ("Built autonomously for " + u) if u is not None \
    else "Built autonomously by Loki Mode"
parts = [lead]
fc = (d.get("files_changed") or {}).get("count", 0)
try:
    fc = int(fc)
except Exception:
    fc = 0
parts.append("%d file%s changed" % (fc, "" if fc == 1 else "s"))
cr = council_ratio(d)
if cr:
    parts.append("%d-of-%d reviewers approved" % (cr[0], cr[1]))
print(" - ".join(parts) + " " + os.environ.get("LOKI_GIST_URL", ""))
PYEOF
)
                if [ -n "$hook" ]; then
                    echo ""
                    echo "Ready to post:"
                    echo "  ${hook}"
                fi

                # Copy-paste "Verified by Loki" markdown badge (word-of-mouth
                # loop). The badge color + message derive ONLY from the receipt's
                # honest headline (honesty.headline), which proof-generator
                # computes deterministically from real facts and which is
                # impossible to forge from an LLM opinion. We exact-match the
                # three headline strings:
                #   VERIFIED            -> brightgreen "verified"
                #   VERIFIED WITH GAPS  -> orange "verified with gaps"
                #   NOT VERIFIED        -> red "not verified"
                # Anything else (missing/null/legacy proof) OMITS the badge: we
                # never paint a non-VERIFIED receipt green, and we do not force a
                # fine old-format proof red. We deliberately do NOT fall back to
                # council.final_verdict here -- that is an LLM verdict and the
                # exact fake-green path the headline exists to override.
                # The badge links to the gist we just published (no new egress).
                # shields.io static/v1 query endpoint avoids structural hyphens.
                local badge
                badge=$(LOKI_PROOF_JSON="$pj" "$_share_py3" -E - <<'PYEOF' 2>/dev/null || true
# The cwd is the checkout being shared: drop it from sys.path before any
# other import, or a committed json.py/os.py could answer this badge text.
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
import json, os
try:
    d = json.load(open(os.environ["LOKI_PROOF_JSON"]))
except Exception:
    d = {}
h = (d.get("honesty") or {}).get("headline")
if isinstance(h, str):
    print(h)
PYEOF
)
                badge=$(_loki_verdict_badge "$badge" "$LOKI_LAST_GIST_URL")
                if [ -n "$badge" ]; then
                    echo ""
                    echo "Markdown badge (paste into a README or PR):"
                    echo "  ${badge}"
                fi
            fi
            exit 0
            ;;
        *)
            echo -e "${RED}Unknown subcommand: ${sub}${NC}"
            echo "Run 'loki proof --help' for usage."
            exit 1
            ;;
    esac
}

# ---------------------------------------------------------------------------
# loki docker - zero-friction Docker host wrapper
#
# Runs loki inside the published image with the SAME experience as the local
# CLI: invoke it in a project folder and it just works. The current directory
# is bind-mounted to /workspace, so .loki/ state (memory, session, queue,
# checkpoints) persists on the host and resume + continuity behave exactly like
# `loki start` locally. Auth is auto-detected (ANTHROPIC_API_KEY if set, else
# the host Claude Code login), so Claude Code subscribers get zero-config runs.
#
# Usage:
#   loki docker start prd.md       # run a build in the container, .loki persists
#   loki docker <any-loki-command> # forwards any command into the container
#   loki docker --dry-run start    # print the docker argv without running
#   loki docker --image IMG ...    # override the image (default published tag)
# ---------------------------------------------------------------------------
cmd_docker() {
    local docker_mod="$_LOKI_SCRIPT_DIR/docker-run.sh"
    if [ ! -f "$docker_mod" ]; then
        echo -e "${RED}Error: docker wrapper module not found at $docker_mod${NC}" >&2
        return 3
    fi

    local sub="${1:-}"
    case "$sub" in
        ""|--help|-h|help)
            echo -e "${BOLD}loki docker${NC} - run loki inside the Docker image, zero config"
            echo ""
            echo "Usage: loki docker [--image IMG] [--dry-run] <loki-command> [args]"
            echo ""
            echo "Runs the published Loki image with your current folder mounted at"
            echo "/workspace, so .loki state persists and resume works exactly like"
            echo "the local CLI. Auth is auto-detected:"
            echo "  1. ANTHROPIC_API_KEY (if set in your environment), else"
            echo "  2. your host Claude Code login (Max/Pro subscription), else"
            echo "  3. an error with setup guidance."
            echo ""
            echo "Examples:"
            echo "  loki docker start prd.md"
            echo "  loki docker status"
            echo "  loki docker --dry-run start prd.md"
            echo ""
            echo "Options:"
            echo "  --image IMG   Override the image (default: ${LOKI_DOCKER_IMAGE:-asklokesh/loki-mode:latest})"
            echo "  --dry-run     Print the docker command that would run, then exit"
            [ "$sub" = "" ] && return 1
            return 0
            ;;
    esac

    if ! command -v docker >/dev/null 2>&1; then
        echo -e "${RED}Error: docker is not installed or not on PATH.${NC}" >&2
        echo "Install Docker Desktop (https://www.docker.com/products/docker-desktop/) and retry." >&2
        return 4
    fi

    # Parse wrapper-only flags (everything else is the loki command to forward).
    # --api is BOTH a forwarded loki flag (start --api) and a wrapper signal to
    # publish the dashboard port, so we detect it without consuming it.
    local dry_run=0
    local with_api=0
    local -a fwd=()
    while [ $# -gt 0 ]; do
        case "$1" in
            --image)
                # Guard against a missing OR flag-shaped value so "--image
                # --dry-run" cannot swallow the next flag as the image ref.
                case "${2:-}" in
                    -*|"") echo "loki docker --image requires a value (e.g., --image asklokesh/loki-mode:latest)" >&2; return 1 ;;
                    *) shift; export LOKI_DOCKER_IMAGE="$1"; shift ;;
                esac
                ;;
            --dry-run) dry_run=1; shift ;;
            --api) with_api=1; fwd+=("$1"); shift ;;
            *) fwd+=("$1"); shift ;;
        esac
    done

    # shellcheck source=/dev/null
    source "$docker_mod"

    local auth; auth="$(loki_docker_detect_auth)"
    local creds=""
    local cleanup_creds=0

    case "$auth" in
        apikey)
            echo -e "${DIM:-}Auth: ANTHROPIC_API_KEY (from environment)${NC}" >&2
            ;;
        oauth)
            creds="$(mktemp "${TMPDIR:-/tmp}/loki-docker-creds.XXXXXX")" || {
                echo -e "${RED}Error: could not create a temp file for credentials.${NC}" >&2
                return 2
            }
            cleanup_creds=1
            # Wipe the token file on any exit path.
            # shellcheck disable=SC2064
            trap "rm -f '$creds' 2>/dev/null || true" EXIT INT TERM
            if ! loki_docker_extract_creds "$creds"; then
                echo -e "${RED}Error: found a host Claude Code login but could not read its token.${NC}" >&2
                return 2
            fi
            echo -e "${DIM:-}Auth: host Claude Code login (auto-detected, mounted read-write)${NC}" >&2
            ;;
        none)
            echo -e "${RED}Error: no auth available for the Docker run.${NC}" >&2
            echo "Do ONE of the following, then retry:" >&2
            echo "  - export ANTHROPIC_API_KEY=sk-ant-...   (get one at https://console.anthropic.com/settings/keys)" >&2
            echo "  - or log in to Claude Code on this host (claude) so loki can reuse it." >&2
            return 5
            ;;
    esac

    # Default the command to 'help' if the user passed only wrapper flags.
    [ ${#fwd[@]} -eq 0 ] && fwd=(help)

    local -a docker_argv=()
    while IFS= read -r line; do docker_argv+=("$line"); done < <(loki_docker_build_argv "$auth" "$creds" "$with_api" "${fwd[@]}")

    if [ "$dry_run" = "1" ]; then
        echo "Would run:"
        # Mask the API key value: dry-run output is exactly what users paste into
        # bug reports / CI logs, so never print the secret.
        local _a
        for _a in "${docker_argv[@]}"; do
            case "$_a" in
                ANTHROPIC_API_KEY=*) printf '  %q' "ANTHROPIC_API_KEY=***" ;;
                *) printf '  %q' "$_a" ;;
            esac
        done
        echo ""
        [ "$cleanup_creds" = "1" ] && rm -f "$creds" 2>/dev/null || true
        return 0
    fi

    # Detect the forwarded loki subcommand (first non-dash token in fwd). This
    # handles `--api start ...` where the wrapper-kept --api precedes `start`.
    local _fwd_sub=""
    local _ft
    for _ft in "${fwd[@]}"; do
        case "$_ft" in
            -*) continue ;;
            *) _fwd_sub="$_ft"; break ;;
        esac
    done

    # Is this docker run launched in background mode? Mirrors the loki `start`
    # background flags (--bg/--background/--detach/-d) that get forwarded into
    # the container. Used to gate dashboard auto-open exactly like run.sh:~10088.
    local _docker_bg=0
    for _ft in "${fwd[@]}"; do
        case "$_ft" in
            --bg|--background|--detach|-d) _docker_bg=1; break ;;
        esac
    done

    # FEAT-DOCKER-DASH / FEAT-DOCKER-PRUNE / FIX-DOCKER-STOP (start path only):
    # the container itself stays dashboard-OFF (no published container port).
    # Instead we drive the HOST dashboard (which already aggregates local +
    # docker runs) so `loki docker start` feels exactly like local `loki start`.
    if [ "$_fwd_sub" = "start" ]; then
        # F3 FEAT-DOCKER-PRUNE: pull latest + prune old loki-mode images before
        # the run. Gated by LOKI_DOCKER_PRUNE (default 1); =0 opts out (and the
        # helper also skips the explicit pull). Best-effort, never blocks a run.
        if [ "${LOKI_DOCKER_PRUNE:-1}" != "0" ] && declare -F loki_docker_pull_and_prune >/dev/null 2>&1; then
            loki_docker_pull_and_prune || true
        fi

        # F2 FEAT-DOCKER-DASH: start/reuse the host dashboard (idempotent: an
        # already-running dashboard short-circuits in cmd_dashboard_start before
        # the port-in-use check, so the picked port is ignored on reuse). Run in
        # a subshell so its internal `exit` paths (already-running -> exit 0,
        # errors -> exit 1) never abort the blocking docker run.
        # CP-LEGACY: open the Control Plane, never the classic server,
        # unless LOKI_LEGACY_DASHBOARD=1 opts back in. Same open gate as before.
        if [ "${LOKI_LEGACY_DASHBOARD:-0}" != "1" ]; then
            local _cp_args=()
            if [ ! -t 1 ] || [ "$_docker_bg" = "1" ] || [ "${LOKI_NO_AUTO_OPEN:-0}" = "1" ]; then
                _cp_args=(--no-open)
            fi
            ( _loki_control_ui ${_cp_args[@]+"${_cp_args[@]}"} ) >&2 || true
        else
            local _dash_port="57374"
            if declare -F loki_docker_pick_host_port >/dev/null 2>&1; then
                _dash_port="$(loki_docker_pick_host_port 2>/dev/null || echo 57374)"
            fi
            ( cmd_dashboard_start --port "$_dash_port" ) || true

            # Auto-open the dashboard in the browser, gated EXACTLY like
            # run.sh:~10088: a TTY on stdout, not background, and not opted out via
            # LOKI_NO_AUTO_OPEN=1. cmd_dashboard_open reads the persisted port file,
            # so the URL is correct even when an existing dashboard was reused.
            if [ -t 1 ] && [ "$_docker_bg" != "1" ] && [ "${LOKI_NO_AUTO_OPEN:-0}" != "1" ]; then
                ( cmd_dashboard_open ) || true
            fi
        fi

        # F4 FIX-DOCKER-STOP (write side): record this run's container/state so
        # `loki stop` in this folder can reap the container. Cleared after the
        # blocking run returns (mirrors the register_host running/stopped bracket).
        # The helper requires the container name + image; compute the deterministic
        # name once (byte-identical to the build-time --name) and pass the resolved image.
        if declare -F loki_docker_write_runstate >/dev/null 2>&1 && declare -F loki_docker_container_name >/dev/null 2>&1; then
            loki_docker_write_runstate "$(loki_docker_container_name)" "${LOKI_DOCKER_IMAGE:-asklokesh/loki-mode:latest}" || true
        fi
    fi

    # Multi-repo unified dashboard (Option B): register THIS project on the host
    # with the real cwd and NO pid, so the existing host dashboard
    # (`loki dashboard`) lists it alongside host `loki start` projects and reads
    # its live status from the bind-mounted .loki/session.json. Best-effort and
    # failure-swallowed: registry bookkeeping must never block a build.
    _loki_docker_register_host running
    local rc _errlog=""
    # Capture output for a failure summary only when no TTY is attached (a TTY
    # run already shows the live output and must not be piped).
    if [ ! -t 1 ] || [ ! -t 0 ]; then
        _errlog="$(mktemp "${TMPDIR:-/tmp}/loki-docker-out.XXXXXX" 2>/dev/null)" || _errlog=""
    fi
    if [ -n "$_errlog" ]; then
        "${docker_argv[@]}" 2>&1 | tee "$_errlog"
        rc=${PIPESTATUS[0]}
    else
        "${docker_argv[@]}"
        rc=$?
    fi
    if [ "$rc" -ne 0 ]; then
        local _last=""
        [ -n "$_errlog" ] && _last="$(grep -v '^[[:space:]]*$' "$_errlog" 2>/dev/null | tail -n 1)"
        echo -e "${RED}loki docker: container exited with code ${rc}.${NC}" >&2
        [ -n "$_last" ] && echo "Last error: ${_last}" >&2
        echo "Fix hint: for PRs set GH_TOKEN=\$(gh auth token), and ensure ANTHROPIC_API_KEY or a Claude login is available, then rerun." >&2
    fi
    [ -n "$_errlog" ] && rm -f "$_errlog" 2>/dev/null
    _loki_docker_register_host stopped
    # F4 FIX-DOCKER-STOP (clear side): runs unconditionally after the blocking
    # call (not gated on rc), same as register_host stopped.
    if [ "$_fwd_sub" = "start" ]; then
        declare -F loki_docker_clear_runstate >/dev/null 2>&1 && loki_docker_clear_runstate || true
    fi
    [ "$cleanup_creds" = "1" ] && rm -f "$creds" 2>/dev/null || true
    return $rc
}

# Host-side project registry bookkeeping for `loki docker` (Option B). Registers
# $(pwd) in ~/.loki/dashboard/projects.json with NO pid (pid=None) so the
# dashboard's "running" decision falls back to .loki/session.json (which the
# container writes into the bind-mounted workspace). $1 = running|stopped.
_loki_docker_register_host() {
    local _state="${1:-running}"
    command -v python3 >/dev/null 2>&1 || return 0
    local _skill="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}"
    LOKI_REG_TARGET="$(pwd)" LOKI_REG_SKILL="$_skill" LOKI_REG_STATE="$_state" \
    python3 - <<'PYDOCKERREG' >/dev/null 2>&1 || true
import os, sys
sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
try:
    from dashboard import registry
    target = os.path.abspath(os.environ["LOKI_REG_TARGET"])
    state = os.environ.get("LOKI_REG_STATE", "running")
    if state == "stopped":
        registry.mark_project_stopped(target)
    else:
        registry.register_project(target)
        reg = registry._load_registry()
        pid_key = registry._generate_project_id(target)
        if pid_key in reg.get("projects", {}):
            # No host pid for a containerized run: the dashboard uses the
            # bind-mounted .loki/session.json to decide "running".
            reg["projects"][pid_key]["pid"] = None
            reg["projects"][pid_key]["status"] = "running"
            registry._save_registry(reg)
except Exception:
    pass
PYDOCKERREG
}

main "$@"
