#!/usr/bin/env bash
#clear
#export PS4='\e[90m+${LINENO} in ${#BASH_SOURCE[@]}>${FUNCNAME[0]}:${BASH_SOURCE[@]##*/} \e[0m'
#set -x

#echo "starting: $0 <LOG_LEVEL=$1>"

### new.method

# ============================================================================
# claudeCode - Claude Code wrapper for oosh
# Makes Claude Code CLI flags easy to use with intuitive method names
# ============================================================================

# Path to claude binary - no need to have it in PATH
CLAUDE_CMD="$HOME/.local/bin/claude"
export FORCE_COLOR=2
export CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=100
unset  COLORTERM
unset  CLAUDECODE

# ── Context window limits — single source of truth ────────────────────────────
# Used by context.*, velocity.*, dashboard. Detection logic (which value applies
# to which session) lives in private.claudeCode.max.tokens.for.jsonl.
# Exported so python subprocesses inherit the constants.
: ${CLAUDE_MAX_TOKENS_DEFAULT:=200000}       # opus/sonnet/haiku base window
: ${CLAUDE_MAX_TOKENS_1M:=1000000}           # sessions launched with [1m] flag
: ${CLAUDE_COMPACT_THRESHOLD_PCT:=90}        # % at which "COMPACT NOW" fires
export CLAUDE_MAX_TOKENS_DEFAULT CLAUDE_MAX_TOKENS_1M CLAUDE_COMPACT_THRESHOLD_PCT

# ── Shared completion helpers ──────────────────────────────────────────────
private.claudeCode.complete.panes() {
  otmux panes -a -F "#{session_name}:#{window_index}.#{pane_index}" 2>/dev/null
}
private.claudeCode.complete.sessionIds() {
  # Filter: emit UUID only if LIVE (in a Claude process) OR REGISTERED (in hivemind.sessions.env).
  # Orphan JSONLs (dead) are excluded from tab completion.
  local claudeProjectsDir="$HOME/.claude/projects"
  local sesFile="${HIVEMIND_SESSIONS:-${CONFIG_PATH:-$HOME/config}/hivemind.sessions.env}"
  local liveUuids=""
  local regUuids=""
  if command -v ps &>/dev/null; then
    liveUuids=$(ps -eo args= 2>/dev/null | grep -i 'claude' | grep -v grep | \
      grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | sort -u)
  fi
  [ -f "$sesFile" ] && regUuids=$(cut -d'|' -f2 "$sesFile" 2>/dev/null | sort -u)

  local sid
  for projectDir in "$claudeProjectsDir"/*; do
    [ ! -d "$projectDir" ] && continue
    local indexFile="$projectDir/sessions-index.json"
    if [ -f "$indexFile" ] && command -v jq &>/dev/null; then
      while read -r sid; do
        [ -z "$sid" ] && continue
        if echo "$liveUuids" | grep -q "$sid" || echo "$regUuids" | grep -q "$sid"; then
          echo "$sid"
        fi
      done < <(jq -r '.entries[].sessionId' "$indexFile" 2>/dev/null)
    else
      local f
      for f in "$projectDir"/*.jsonl; do
        [ -f "$f" ] || continue
        sid=$(basename "$f" .jsonl)
        if echo "$liveUuids" | grep -q "$sid" || echo "$regUuids" | grep -q "$sid"; then
          echo "$sid"
        fi
      done
    fi
  done
}
private.claudeCode.complete.roleNames() {
  local reg="${HIVEMIND_REGISTRY:-${CONFIG_PATH:-$HOME/config}/hivemind.roles.env}"
  [ -f "$reg" ] && cut -d'|' -f2 "$reg" 2>/dev/null | sort -u
}

private.claudeCode.decode.projectHash() { # <hash> # decode project dir hash to path: -Users-Shared → /Users/Shared
  echo "$1" | sed 's/^-/\//' | sed 's/-/\//g'
}

private.claudeCode.resolve.projectDir() { # <sessionId> # find and cd to the project directory for a session UUID
  local sessionId="$1"
  [ -z "$sessionId" ] && return 1
  local claudeProjectsDir="$HOME/.claude/projects"
  # Find ALL project dirs containing this JSONL, pick the longest path (most specific)
  local bestPath="" bestLen=0
  for projectDir in "$claudeProjectsDir"/*/; do
    [ -f "${projectDir}${sessionId}.jsonl" ] || continue
    local projectHash
    projectHash=$(basename "$projectDir")
    local decodedPath
    decodedPath=$(private.claudeCode.decode.projectHash "$projectHash")
    if [ -d "$decodedPath" ] && [ ${#decodedPath} -gt $bestLen ]; then
      bestPath="$decodedPath"
      bestLen=${#decodedPath}
    fi
  done
  if [ -n "$bestPath" ]; then
    cd "$bestPath"
    return 0
  fi
  return 1
}

claudeCode.sessions() # # list all available sessions (interactive picker)
{
  "$CLAUDE_CMD" --resume
}

claudeCode.list() # <?format:tree|json> # list all Claude Code sessions on this host (tree default; json delegates to list.json)
{
  local CLAUDE_PROJECTS_DIR="$HOME/.claude/projects"
  local format="${1:-tree}"

  # Backward-compat: accept legacy --json form.
  # OOSH first principle (T-ARCH-5): no --flag args. New form is positional 'json'.
  [ "$format" = "--json" ] && format="json"
  [ "$format" != "json" ] && format="tree"

  if [ ! -d "$CLAUDE_PROJECTS_DIR" ]; then
    echo "No Claude projects directory found: $CLAUDE_PROJECTS_DIR"
    return 1
  fi

  if [ "$format" = "json" ]; then
    claudeCode.list.json
    return $?
  fi

  echo ""
  echo "┌────────────────────────────────────────────────────────────────────────────────────────┐"
  echo "│                        CLAUDE CODE SESSIONS                                            │"
  echo "├────────────────────────────────────────────────────────────────────────────────────────┤"
  echo "│ UUID                                    Agent            Pane              Last Active │"
  echo "└────────────────────────────────────────────────────────────────────────────────────────┘"
  echo ""

  

  local totalSessions=0
  local totalProjects=0

  # Precompute live UUIDs once — used to detect DEAD (orphan JSONL) entries.
  local liveUuids=""
  if command -v ps &>/dev/null; then
    liveUuids=$(ps -eo args= 2>/dev/null | grep -i 'claude' | grep -v grep | \
      grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | sort -u)
  fi

  for projectDir in "$CLAUDE_PROJECTS_DIR"/*; do
    [ ! -d "$projectDir" ] && continue

    # Option B: skip the dash dir (queue-operation logs, not agent sessions)
    [ "$(basename "$projectDir")" = "-" ] && continue

    local projectName=$(basename "$projectDir")
    local indexFile="$projectDir/sessions-index.json"
    local decodedPath=$(private.claudeCode.decode.projectHash "$projectName")

    ((totalProjects++))

    echo "📁 $decodedPath"

      # Parse JSONL filenames + registry cross-reference (works with or without jq)
      local jsonlFiles=("$projectDir"/*.jsonl)
      local jsonlCount=${#jsonlFiles[@]}
      [ ! -f "${jsonlFiles[0]}" ] && jsonlCount=0
      if [ "$jsonlCount" -gt 0 ]; then
        local idx=0
        for f in "${jsonlFiles[@]}"; do
          [ -f "$f" ] || continue
          # Option A: skip queue-operation logs (not agent sessions)
          head -1 "$f" 2>/dev/null | grep -q '"type":"queue-operation"' && continue
          ((idx++))
          ((totalSessions++))
          local sid=$(basename "$f" .jsonl)
          local prefix="├──"
          [ "$idx" -eq "$jsonlCount" ] && prefix="└──"
          # Cross-reference with hiveMind registry for role name + pane
          local regRole="" regPane=""
          local regFile="${CONFIG_PATH:-$HOME/config}/hivemind.sessions.env"
          if [ -f "$regFile" ]; then
            regPane=$(grep "|${sid}$" "$regFile" 2>/dev/null | tail -1 | cut -d'|' -f1)
          fi
          local roleFile="${CONFIG_PATH:-$HOME/config}/hivemind.roles.env"
          if [ -n "$regPane" ] && [ -f "$roleFile" ]; then
            regRole=$(grep "^${regPane}|" "$roleFile" 2>/dev/null | head -1 | cut -d'|' -f2)
          fi
          # Get name from JSONL custom-title (pure bash — no python/jq)
          local sessionName=""
          local rawTitle=$(grep '"customTitle"' "$f" 2>/dev/null | tail -1)
          if [ -n "$rawTitle" ]; then
            sessionName=$(echo "$rawTitle" | grep -oE '"customTitle":"[^"]*"' | head -1 | sed 's/"customTitle":"//; s/"$//')
          fi
          # Strip @model suffix (e.g. oosh-expert@opus → oosh-expert)
          local label="${sessionName:-${regRole:-}}"
          label="${label%%@*}"
          local paneLabel=""
          [ -n "$regPane" ] && paneLabel="($regPane)"
          # Last active from file mtime
          local lastActive
          lastActive=$(stat -f '%Sm' -t '%b %d %H:%M' "$f" 2>/dev/null || stat -c '%y' "$f" 2>/dev/null | cut -c1-16)
          # Color + status marker:
          #   RED [DEAD]        — no pane AND no live process
          #   CYAN [FORK-READY] — has pane + role + 60..80% ctx used (remaining 20..40)
          #   GREEN             — has pane (active)
          #   YELLOW            — no pane, mtime < 24h (recent)
          #   GRAY              — old/inactive
          local color="${GRAY}"
          local marker=""
          if [ -z "$regPane" ] && ! echo "$liveUuids" | grep -q "$sid"; then
            color="${BOLD_RED}"
            marker=" [DEAD]"
          elif [ -n "$regPane" ]; then
            color="${BOLD_GREEN}"
            # Check fork-ready: role known + context remaining in 20..40 (= 60..80% used)
            if [ -n "$regRole" ] && type -t private.claudeCode.context.from.jsonl &>/dev/null; then
              local ctxR
              ctxR=$(private.claudeCode.context.from.jsonl "$f" 2>/dev/null)
              if [[ "$ctxR" =~ ^[0-9]+(\.[0-9]+)?$ ]] && \
                 awk "BEGIN{exit !($ctxR>=20 && $ctxR<=40)}"; then
                color="${BOLD_CYAN}"
                marker=" [FORK-READY]"
              fi
            fi
          else
            local mtime_epoch
            mtime_epoch=$(stat -f '%m' "$f" 2>/dev/null || stat -c '%Y' "$f" 2>/dev/null)
            local now_epoch
            now_epoch=$(date +%s)
            if [ -n "$mtime_epoch" ] && [ $(( now_epoch - mtime_epoch )) -lt 86400 ]; then
              color="${BOLD_YELLOW}"
            fi
          fi
          local RST=$'\e[0m'
          printf "%s ${color}%-36s${RST}  ${BOLD_WHITE}%-16s${RST} ${BOLD_CYAN}%-18s${RST} ${color}%s${RST}${color}%s${RST}\n" "$prefix" "$sid" "${label}" "${paneLabel}" "${lastActive}" "${marker}"
        done
      else
        echo "└── (no sessions)"
      fi
    echo ""
  done

  echo "─────────────────────────────────────────────────────────────────────────────"
  echo "Total: $totalProjects projects"
}

claudeCode.list.completion.format() {
  echo tree
  echo json
}

claudeCode.list.json() # # output session list as JSON
{
  local CLAUDE_PROJECTS_DIR="$HOME/.claude/projects"

  if command -v python3 >/dev/null 2>&1; then
    python3 -c "
import json, os, glob
projects_dir = '$CLAUDE_PROJECTS_DIR'
result = []
for proj in sorted(glob.glob(os.path.join(projects_dir, '*'))):
    if not os.path.isdir(proj): continue
    proj_name = os.path.basename(proj)
    decoded = '/' + proj_name.lstrip('-').replace('-', '/')
    idx = os.path.join(proj, 'sessions-index.json')
    if os.path.isfile(idx):
        try:
            data = json.load(open(idx))
            for e in data.get('entries', []):
                e['directory'] = decoded
                result.append(e)
        except: pass
    else:
        for f in glob.glob(os.path.join(proj, '*.jsonl')):
            sid = os.path.splitext(os.path.basename(f))[0]
            result.append({'sessionId': sid, 'directory': decoded})
print(json.dumps(result, indent=2))
" 2>/dev/null
  elif command -v jq >/dev/null 2>&1; then
    echo "["
    local first=true
    for projectDir in "$CLAUDE_PROJECTS_DIR"/*; do
      [ ! -d "$projectDir" ] && continue
      local decodedPath=$(private.claudeCode.decode.projectHash "$(basename "$projectDir")")
      local indexFile="$projectDir/sessions-index.json"
      [ -f "$indexFile" ] && jq -r --arg dir "$decodedPath" \
        '.entries[] | {sessionId, firstPrompt, messageCount, modified, gitBranch, directory: $dir}' \
        "$indexFile" 2>/dev/null
    done
    echo "]"
  else
    error.log "Neither python3 nor jq available for JSON output"
    return 1
  fi
}

claudeCode.join() # <session> # resume a specific session by name or UUID
{
  local session="$1"
  if [ -n "$session" ]; then
    shift
    # If not a UUID, try role→pane→UUID or pane→UUID lookup
    if ! [[ "$session" =~ ^[0-9a-f]{8}-[0-9a-f]{4} ]]; then
      local reg="${HIVEMIND_REGISTRY:-${CONFIG_PATH:-$HOME/config}/hivemind.roles.env}"
      local ses="${HIVEMIND_SESSIONS:-${CONFIG_PATH:-$HOME/config}/hivemind.sessions.env}"
      local foundUuid=""
      # Try as pane target first (new schema: pane|UUID)
      [ -f "$ses" ] && foundUuid=$(grep "^${session}|" "$ses" 2>/dev/null | head -1 | cut -d'|' -f2)
      # Try as role name: resolve role→pane→UUID. Registry is 3-field
      # (pane|role|epoch) since the TTL addition — match field 2 by exact
      # equality via awk instead of $-anchored grep (which the epoch breaks).
      if [ -z "$foundUuid" ] && [ -f "$reg" ]; then
        local pane
        pane=$(awk -F'|' -v n="$session" '$2==n {print $1; exit}' "$reg" 2>/dev/null)
        [ -n "$pane" ] && [ -f "$ses" ] && foundUuid=$(grep "^${pane}|" "$ses" 2>/dev/null | head -1 | cut -d'|' -f2)
      fi
      [ -n "$foundUuid" ] && session="$foundUuid"
    fi
    # Update sessions file with the UUID being resumed (keyed by pane)
    local paneTarget
    paneTarget=$(otmux pane.get.target 2>/dev/null)
    if [ -n "$paneTarget" ]; then
      local ses="${HIVEMIND_SESSIONS:-${CONFIG_PATH:-$HOME/config}/hivemind.sessions.env}"
      if [ -f "$ses" ]; then
        grep -v "^${paneTarget}|" "$ses" > "${ses}.tmp" 2>/dev/null
        mv "${ses}.tmp" "$ses"
      fi
      echo "${paneTarget}|${session}" >> "$ses"
    fi
    # cd to the project directory where the JSONL lives (required for Claude to find it)
    private.claudeCode.resolve.projectDir "$session" 2>/dev/null
    "$CLAUDE_CMD" --resume "$session" --model "claude-opus-4-6[1m]" "$@"
  else
    # No session specified, show picker
    "$CLAUDE_CMD" --resume
  fi
}
claudeCode.join.completion.session() {
  private.claudeCode.complete.sessionIds
}

claudeCode.join.byID() # <sessionId> # resume session directly by UUID
{
  local sessionId="$1"
  [ -z "$sessionId" ] && { error.log "usage: claudeCode join.byID <sessionId>"; return 1; }
  # SC-E.2 P3: sessionId flows to claude --resume; reject garbage at boundary
  # instead of relying on Claude failing downstream.
  if ! this.isUuid "$sessionId"; then
    error.log "claudeCode.join.byID: invalid UUID '$sessionId' (expected 8-4-4-4-12 hex)"
    return 1
  fi
  shift
  "$CLAUDE_CMD" --resume "$sessionId" "$@"
}
claudeCode.join.byID.completion.sessionId() {
  private.claudeCode.complete.sessionIds
}

# ─── Shared resolution helpers (DRY — used by both join.by and fork.by) ─────

private.claudeCode.resolve.byName() { # <name> # role name → UUID (via registry → sessions.env)
  local name="$1"
  local reg="${HIVEMIND_REGISTRY:-${CONFIG_PATH:-$HOME/config}/hivemind.roles.env}"
  local ses="${HIVEMIND_SESSIONS:-${CONFIG_PATH:-$HOME/config}/hivemind.sessions.env}"
  # Registry is 3-field (pane|role|epoch) since the TTL addition — match
  # field 2 by exact equality via awk instead of $-anchored grep (which
  # the trailing epoch broke). Robust against future field additions.
  local pane
  pane=$(awk -F'|' -v n="$name" '$2==n {print $1; exit}' "$reg" 2>/dev/null)
  if [ -z "$pane" ]; then
    error.log "no pane found for role '$name' in registry"
    return 1
  fi
  local uuid
  uuid=$(grep "^${pane}|" "$ses" 2>/dev/null | head -1 | cut -d'|' -f2)
  if [ -z "$uuid" ]; then
    error.log "no session UUID found for pane '$pane' (role '$name')"
    return 1
  fi
  echo "$uuid"
}

private.claudeCode.resolve.byPane() { # <pane> # pane target → UUID (via session.id → Controller probe fallback)
  # NOTE: This helper itself is flagged as Controller-leak in A1.2 plan and
  # scheduled for removal in favor of hiveMind.agent.* methods. Until then,
  # the probe fallback delegates to hiveMind.agent.session.probe (Controller).
  local pane="$1"
  local sid
  sid=$(claudeCode.session.id "$pane" 2>/dev/null)
  if [ -z "$sid" ]; then
    info.log "session.id failed, trying hiveMind.agent.session.probe (slow ~3s)..."
    sid=$(hiveMind agent.session.probe "$pane" 2>/dev/null)
  fi
  if [ -z "$sid" ]; then
    error.log "could not resolve session UUID for pane '$pane'"
    return 1
  fi
  echo "$sid"
}

# ─── join.byName / join.byPane (refactored to shared helpers) ─────────────

claudeCode.join.byName() # <name> # resume session by role name (role→pane→UUID lookup)
{
  local name="$1"
  [ -z "$name" ] && { error.log "usage: claudeCode join.byName <name>"; return 1; }
  shift
  local uuid
  uuid=$(private.claudeCode.resolve.byName "$name") || return 1
  "$CLAUDE_CMD" --resume "$uuid" "$@"
}
claudeCode.join.byName.completion.name() {
  private.claudeCode.complete.roleNames
}

claudeCode.join.byPane() # <pane> # resume session by resolving pane → UUID
{
  local pane="$1"
  [ -z "$pane" ] && { error.log "usage: claudeCode join.byPane <pane>"; return 1; }
  # SC-E.2 P3: pane format check before resolver lookup.
  if ! this.isPaneTarget "$pane"; then
    error.log "claudeCode.join.byPane: invalid pane '$pane' (expected session:window.pane or %N)"
    return 1
  fi
  shift
  local uuid
  uuid=$(private.claudeCode.resolve.byPane "$pane") || return 1
  "$CLAUDE_CMD" --resume "$uuid" "$@"
}
claudeCode.join.byPane.completion.pane() {
  private.claudeCode.complete.panes
}

# ─── fork.byID / fork.byName / fork.byPane ────────────────────────────────

claudeCode.fork() # <sessionId> # fork session by UUID with new ID, preserving conversation history
{
  local sessionId="$1"
  [ -z "$sessionId" ] && { error.log "usage: claudeCode fork <sessionId>"; return 1; }
  # SC-E.2 P3: sessionId flows to claude --resume; reject garbage at boundary.
  if ! this.isUuid "$sessionId"; then
    error.log "claudeCode.fork: invalid UUID '$sessionId' (expected 8-4-4-4-12 hex)"
    return 1
  fi
  shift
  private.claudeCode.resolve.projectDir "$sessionId" 2>/dev/null
  "$CLAUDE_CMD" --resume "$sessionId" --fork-session --model "claude-opus-4-6[1m]" "$@"
}
claudeCode.fork.completion.sessionId() {
  private.claudeCode.complete.sessionIds
}

claudeCode.fork.byID() # <sessionId> # fork session directly by UUID (alias for fork)
{
  claudeCode.fork "$@"
}
claudeCode.fork.byID.completion.sessionId() {
  private.claudeCode.complete.sessionIds
}

claudeCode.fork.byName() # <name> # fork session by role name (role→pane→UUID→fork)
{
  local name="$1"
  [ -z "$name" ] && { error.log "usage: claudeCode fork.byName <name>"; return 1; }
  shift
  local uuid
  uuid=$(private.claudeCode.resolve.byName "$name") || return 1
  private.claudeCode.resolve.projectDir "$uuid" 2>/dev/null
  "$CLAUDE_CMD" --resume "$uuid" --fork-session --model "claude-opus-4-6[1m]" "$@"
}
claudeCode.fork.byName.completion.name() {
  private.claudeCode.complete.roleNames
}

claudeCode.fork.byPane() # <pane> # fork session by resolving pane → UUID → fork
{
  local pane="$1"
  [ -z "$pane" ] && { error.log "usage: claudeCode fork.byPane <pane>"; return 1; }
  # SC-E.2 P3: pane format check before resolver lookup.
  if ! this.isPaneTarget "$pane"; then
    error.log "claudeCode.fork.byPane: invalid pane '$pane' (expected session:window.pane or %N)"
    return 1
  fi
  shift
  local uuid
  uuid=$(private.claudeCode.resolve.byPane "$pane") || return 1
  private.claudeCode.resolve.projectDir "$uuid" 2>/dev/null
  "$CLAUDE_CMD" --resume "$uuid" --fork-session --model "claude-opus-4-6[1m]" "$@"
}
claudeCode.fork.byPane.completion.pane() {
  private.claudeCode.complete.panes
}

claudeCode.fork.to() # <pane> <?role> # one-shot recovery: pick best JSONL for role, fork into pane, rename, register, set title, send boot
{
  # Single command replacing the 10-step manual recovery process.
  # Delegates the orchestration to hiveMind.agent.fork.best (Controller) — this
  # is a Model-side facade so Tron's mental model "claudeCode fork.to <pane>"
  # works as expected. Per A1.2 design, the Model itself stays pane-free; the
  # Controller does the View I/O.
  #
  # If <role> is omitted, derive from pane title (after stripping prefixes
  # like '✳ ' or '⠐ ' that Claude adds during 'thinking' states).
  local pane="$1"
  local role="$2"
  if [ -z "$pane" ]; then
    error.log "Usage: claudeCode fork.to <pane> <?role>"
    return 1
  fi

  # SC-E.2 P3: pane writes to registry via downstream hiveMind.agent.fork.best;
  # role flows into hivemind.roles.env. Guard at the boundary.
  if ! this.isPaneTarget "$pane"; then
    error.log "claudeCode.fork.to: invalid pane '$pane' (expected session:window.pane or %N)"
    return 1
  fi
  if [ -n "$role" ] && ! this.isRoleName "$role"; then
    error.log "claudeCode.fork.to: invalid role '$role' (alphanumeric + dot/dash/underscore, max 40)"
    return 1
  fi

  if [ -z "$role" ]; then
    local title
    title=$("$OOSH_DIR/otmux" pane.get "$pane" '#{pane_title}' 2>/dev/null)
    # Strip Claude's animated prefixes + '@model' suffix
    role=$(echo "$title" | sed -E 's/^[^a-zA-Z]*//;s/@.*$//;s/[[:space:]]+$//')
    if [ -z "$role" ]; then
      error.log "Cannot derive role from pane title — pass <role> explicitly"
      return 1
    fi
    info.log "Derived role from pane title: $role"
  fi

  "$OOSH_DIR/hiveMind" agent.fork.best "$role" "$pane"
}
claudeCode.fork.to.completion.pane() {
  private.claudeCode.complete.panes
}
claudeCode.fork.to.completion.role() {
  "$OOSH_DIR/hiveMind" roles.list.uuids.completion.role 2>/dev/null
}

claudeCode.continue() # # continue the most recent session without picker
{
  "$CLAUDE_CMD" --continue "$@"
}

claudeCode.new() # <?prompt> # start a fresh conversation with optional initial prompt
{
  if [ -n "$1" ]; then
    "$CLAUDE_CMD" --print "$@"
  else
    "$CLAUDE_CMD"
  fi
}

claudeCode.print() # <prompt> # non-interactive: print response and exit
{
  "$CLAUDE_CMD" --print "$@"
}

claudeCode.dangerously() # <prompt> # skip all permission prompts (use with caution!)
{
  "$CLAUDE_CMD" --dangerously-skip-permissions "$@"
}

claudeCode.verbose() # <?prompt> # run with verbose output
{
  "$CLAUDE_CMD" --verbose "$@"
}

claudeCode.model() # <model> <?prompt> # use specific model (sonnet, opus, haiku)
{
  local model="$1"
  if [ -n "$model" ]; then
    shift
    "$CLAUDE_CMD" --model "$model" "$@"
  else
    error.log "usage: cc model <sonnet|opus|haiku> [prompt]"
    return 1
  fi
}
claudeCode.model.completion.model() {
  echo "sonnet"
  echo "opus"
  echo "haiku"
}

claudeCode.model.list() # # list available model aliases
{
  echo "opus"
  echo "sonnet"
  echo "haiku"
}
claudeCode.model.list.completion() { :; }

claudeCode.model.set() # <pane> <model> # switch model in running session via /model
{
  local pane="$1"
  local model="$2"

  [ -z "$pane" ] || [ -z "$model" ] && { error.log "Usage: claudeCode model.set <pane> <model>"; return 1; }

  # Validate model alias
  case "$model" in
    opus|sonnet|haiku) ;;
    *) error.log "Invalid model: $model (use opus, sonnet, haiku)"; return 1 ;;
  esac

  # Send /model command to the pane
  "$OOSH_DIR/otmux" send.enter "$pane" "/model $model"
  info.log "Sent /model $model to $pane"
}
claudeCode.model.set.completion.pane() {
  private.claudeCode.complete.panes
}
claudeCode.model.set.completion.model() {
  claudeCode.model.list
}

claudeCode.model.get() # <pane> # get current model from running session
{
  local pane="$1"
  [ -z "$pane" ] && { error.log "Usage: claudeCode model.get <pane>"; return 1; }

  # Capture TUI status bar — Claude Code shows model in status line
  local content
  content=$("$OOSH_DIR/otmux" pane.capture "$pane" 10)

  # Parse model from status bar (format: "Model: opus" or similar)
  local model
  model=$(echo "$content" | grep -oiE '(opus|sonnet|haiku)' | head -1 | tr '[:upper:]' '[:lower:]')

  if [ -n "$model" ]; then
    RESULT="$model"
    echo "$model"
    return 0
  fi

  # Fallback: unknown (TUI may not show model)
  RESULT="unknown"
  echo "unknown"
  return 1
}
claudeCode.model.get.completion.pane() {
  private.claudeCode.complete.panes
}

claudeCode.opus() # <?prompt> # use opus 1M context model
{
  "$CLAUDE_CMD" --model "claude-opus-4-6[1m]" "$@"
}

claudeCode.sonnet() # <?prompt> # use sonnet model
{
  "$CLAUDE_CMD" --model sonnet "$@"
}

claudeCode.haiku() # <?prompt> # use haiku model
{
  "$CLAUDE_CMD" --model haiku "$@"
}

claudeCode.chat() # <?prompt> # interactive chat mode (default)
{
  "$CLAUDE_CMD" "$@"
}

claudeCode.help() # # show claude help
{
  "$CLAUDE_CMD" --help
}

claudeCode.version() # # show claude version
{
  "$CLAUDE_CMD" --version
}

claudeCode.config() # # open claude settings/config
{
  "$CLAUDE_CMD" config
}

claudeCode.doctor() # # run claude doctor to check setup
{
  "$CLAUDE_CMD" doctor
}

claudeCode.mcp() # # manage MCP servers
{
  "$CLAUDE_CMD" mcp "$@"
}

claudeCode.tools.allowed() # <tools> # restrict to specific tools (comma-separated)
{
  local tools="$1"
  if [ -n "$tools" ]; then
    shift
    "$CLAUDE_CMD" --allowedTools "$tools" "$@"
  else
    error.log "usage: cc allowedTools <tool1,tool2,...> [prompt]"
    return 1
  fi
}

claudeCode.tools.disallowed() # <tools> # block specific tools (comma-separated)
{
  local tools="$1"
  if [ -n "$tools" ]; then
    shift
    "$CLAUDE_CMD" --disallowedTools "$tools" "$@"
  else
    error.log "usage: cc disallowedTools <tool1,tool2,...> [prompt]"
    return 1
  fi
}

claudeCode.turns.max() # <turns> <?prompt> # limit conversation turns
{
  local turns="$1"
  if [ -n "$turns" ]; then
    shift
    "$CLAUDE_CMD" --max-turns "$turns" "$@"
  else
    error.log "usage: cc maxTurns <number> [prompt]"
    return 1
  fi
}

claudeCode.system.prompt() # <prompt> <?message> # set custom system prompt
{
  local sysprompt="$1"
  if [ -n "$sysprompt" ]; then
    shift
    "$CLAUDE_CMD" --system-prompt "$sysprompt" "$@"
  else
    error.log "usage: cc systemPrompt <prompt> [message]"
    return 1
  fi
}

claudeCode.system.prompt.append() # <prompt> <?message> # append to system prompt
{
  local sysprompt="$1"
  if [ -n "$sysprompt" ]; then
    shift
    "$CLAUDE_CMD" --append-system-prompt "$sysprompt" "$@"
  else
    error.log "usage: cc appendSystemPrompt <prompt> [message]"
    return 1
  fi
}

claudeCode.output() # <format> <?prompt> # set output format (text, json, stream-json)
{
  local format="$1"
  if [ -n "$format" ]; then
    shift
    "$CLAUDE_CMD" --output-format "$format" "$@"
  else
    error.log "usage: cc output <text|json|stream-json> [prompt]"
    return 1
  fi
}
claudeCode.output.completion.format() {
  echo "text"
  echo "json"
  echo "stream-json"
}

claudeCode.json() # <?prompt> # output as JSON
{
  "$CLAUDE_CMD" --output-format json "$@"
}

claudeCode.pipe() # # read from stdin (for piping)
{
  "$CLAUDE_CMD" -p "$@"
}

claudeCode.init() # <?path> # initialize claude in a directory
{
  if [ -n "$1" ]; then
    "$CLAUDE_CMD" init "$1"
  else
    "$CLAUDE_CMD" init
  fi
}

claudeCode.update() # # update claude code
{
  "$CLAUDE_CMD" update
}

claudeCode.login() # # login to claude
{
  "$CLAUDE_CMD" login
}

claudeCode.logout() # # logout from claude
{
  "$CLAUDE_CMD" logout
}

claudeCode.install() # # install claude code from web for linux
{
  local INSTALL_DIR="${HOME}/.local/bin"
  local CONFIG_FILE="${CONFIG:-$HOME/config/user.env}"

  # Check if already installed
  if command -v claude &> /dev/null; then
    info.log "Claude Code is already installed: $(which claude)"
    "$CLAUDE_CMD" --version
    echo ""
    read -p "Reinstall/update anyway? [y/N] " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
      info.log "Installation cancelled"
      return 0
    fi
  fi

  info.log "Installing Claude Code for Linux..."

  # Ensure curl is installed (silent if already present)
  oo cmd curl

  # Download and run the official installer
  info.log "Downloading and running official installer..."
  curl -fsSL https://claude.ai/install.sh | bash

  if [ $? -ne 0 ]; then
    error.log "Installation failed"
    return 1
  fi

  # Check if ~/.local/bin is in PATH via config
  if [ -f "$CONFIG_FILE" ]; then
    if ! grep -q '\.local/bin' "$CONFIG_FILE"; then
      info.log "Adding ~/.local/bin to PATH in config..."
      # Read current PATH from config
      local currentPath=$(grep '^export PATH=' "$CONFIG_FILE" | head -1)
      if [ -n "$currentPath" ]; then
        # Check if .local/bin is already in the PATH value
        if ! echo "$currentPath" | grep -q '\.local/bin'; then
          # Append to existing PATH
          sed -i 's|^export PATH="\(.*\)"$|export PATH="\1:'"$HOME"'/.local/bin"|' "$CONFIG_FILE"
          info.log "Updated PATH in $CONFIG_FILE"
        fi
      else
        # Add new PATH export
        echo "export PATH=\"\$PATH:$HOME/.local/bin\"" >> "$CONFIG_FILE"
        info.log "Added PATH to $CONFIG_FILE"
      fi
    else
      info.log "~/.local/bin already in config PATH"
    fi
  else
    warn.log "Config file not found at $CONFIG_FILE"
    warn.log "Add ~/.local/bin to your PATH manually if needed"
  fi

  # Add to current session PATH if not present
  if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
    export PATH="$PATH:$INSTALL_DIR"
    info.log "Added $INSTALL_DIR to current session PATH"
  fi

  # Create claude.env with defaults if missing
  local claudeEnv="${CONFIG_PATH:-$HOME/config}/claude.env"
  if [ ! -f "$claudeEnv" ]; then
    echo 'export CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=100' > "$claudeEnv"
    info.log "Created $claudeEnv (autocompact disabled)"
  fi
  # Ensure user.env sources claude.env
  if [ -f "$CONFIG_FILE" ] && ! grep -q 'claude.env' "$CONFIG_FILE"; then
    echo 'source $CONFIG_PATH/claude.env' >> "$CONFIG_FILE"
    info.log "Added claude.env source to $CONFIG_FILE"
  fi
  # Source in current session
  source "$claudeEnv" 2>/dev/null

  # Verify installation
  if command -v claude &> /dev/null; then
    success.log "Claude Code installed successfully!"
    "$CLAUDE_CMD" --version
    echo ""
    info.log "Run 'claudeCode login' to authenticate"
  else
    error.log "Installation completed but 'claude' command not found"
    error.log "You may need to restart your shell or add ~/.local/bin to PATH"
    return 1
  fi
}

claudeCode.uninstall() # # uninstall claude code
{
  local INSTALL_DIR="${HOME}/.local"
  local CLAUDE_DIR="${INSTALL_DIR}/share/claude"
  local CLAUDE_BIN="${INSTALL_DIR}/bin/claude"

  if [ ! -f "$CLAUDE_BIN" ] && [ ! -d "$CLAUDE_DIR" ]; then
    warn.log "Claude Code does not appear to be installed"
    return 0
  fi

  echo "This will remove:"
  [ -f "$CLAUDE_BIN" ] && echo "  - $CLAUDE_BIN"
  [ -d "$CLAUDE_DIR" ] && echo "  - $CLAUDE_DIR"
  echo ""
  read -p "Are you sure you want to uninstall Claude Code? [y/N] " -n 1 -r
  echo

  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    info.log "Uninstall cancelled"
    return 0
  fi

  info.log "Removing Claude Code..."

  # Remove symlink/binary
  if [ -f "$CLAUDE_BIN" ] || [ -L "$CLAUDE_BIN" ]; then
    rm -f "$CLAUDE_BIN"
    info.log "Removed $CLAUDE_BIN"
  fi

  # Remove installation directory
  if [ -d "$CLAUDE_DIR" ]; then
    rm -rf "$CLAUDE_DIR"
    info.log "Removed $CLAUDE_DIR"
  fi

  success.log "Claude Code uninstalled successfully"
  info.log "Note: ~/.claude config directory was preserved"
}

# ─────────────────────────────────────────────────────────────────────────────
# AGENT SUPPORT
# ─────────────────────────────────────────────────────────────────────────────

claudeCode.process.find() # <pane> # find Claude Code PID running in a tmux pane
{
  local target="$1"
  [ -z "$target" ] && return 1

  local tty
  tty=$(otmux pane.get "$target" "#{pane_tty}" 2>/dev/null)
  [ -z "$tty" ] && return 1
  local ttyShort="${tty#/dev/}"

  # Search full command line (args) for 'claude' on this TTY.
  # Using args instead of comm avoids truncation when Claude is installed
  # at a long path like ~/.local/bin/claude.
  local pid
  pid=$(ps -eo pid,tty,args 2>/dev/null | awk -v t="$ttyShort" '$2 == t' | grep -i 'claude' | awk '{print $1}' | head -1)
  [ -n "$pid" ] && echo "$pid" && return 0
  return 1
}

claudeCode.process.running() # <pane> # check if Claude Code is running in a tmux pane
{
  claudeCode.process.find "$1" > /dev/null 2>&1
}

# ─────────────────────────────────────────────────────────────────────────────
# UUID DISCOVERY — single source of truth for pane → session UUID
# ─────────────────────────────────────────────────────────────────────────────
# discover is the ONE non-invasive function. All other resolvers wrap it.
# No /status TUI hijack. No ps-args (stale for forks). Correlates:
#   pane customTitle (from otmux title) + cwd (from pane) + JSONL first-line
# State classification:
#   live    — JSONL exists, mtime < 120s, Claude process running in pane
#   stable  — JSONL exists, Claude process running, mtime >= 120s
#   stale   — JSONL exists but no Claude process (zombie after compact/exit)
#   broken  — sessions.env references UUID but no JSONL anywhere
#   unknown — no pane, no Claude, no JSONL match
# ─────────────────────────────────────────────────────────────────────────────

private.claudeCode.session.discover() # <pane> # prints "<uuid>|<state>|<customTitle>" — single non-invasive source of truth
{
  local pane="$1"
  [ -z "$pane" ] && return 1

  # Pane must exist
  otmux pane.get "$pane" '#{pane_id}' >/dev/null 2>&1 || { echo "||unknown"; return 1; }

  # Strip spinner/accept-edits markers from pane title
  local title
  title=$(otmux pane.get "$pane" '#{pane_title}' 2>/dev/null)
  local cleanTitle="${title#✳ }"; cleanTitle="${cleanTitle# }"
  cleanTitle="${cleanTitle#⏵⏵ }"; cleanTitle="${cleanTitle# }"

  # Pane cwd — used to disambiguate multiple JSONLs sharing customTitle
  local paneCwd
  paneCwd=$(otmux pane.get "$pane" '#{pane_current_path}' 2>/dev/null)

  # Claude process check
  local pid
  pid=$(claudeCode.process.find "$pane" 2>/dev/null)

  # Find newest JSONL whose LATEST customTitle matches current pane title.
  # Using the last customTitle (not any occurrence) is critical: JSONLs accumulate
  # a new custom-title entry on every /rename, so a file may contain many historical
  # titles. Only the final one reflects the session's current identity.
  local bestUuid="" bestMtime=0 bestCwdMatch=0
  local f uuid lastTitle cwdMatch m
  if [ -n "$cleanTitle" ]; then
    for f in "$HOME/.claude/projects"/*/*.jsonl; do
      [ -f "$f" ] || continue
      # Extract the last customTitle in this JSONL
      lastTitle=$(grep -o '"customTitle":"[^"]*"' "$f" 2>/dev/null | tail -1 | sed 's/.*":"//;s/"$//')
      [ -z "$lastTitle" ] && continue
      [ "$lastTitle" = "$cleanTitle" ] || continue
      uuid=$(basename "$f" .jsonl)
      # cwd disambiguator — first line usually has "cwd":"/path"
      cwdMatch=0
      if [ -n "$paneCwd" ] && head -1 "$f" 2>/dev/null | grep -q "\"cwd\":\"${paneCwd}\""; then
        cwdMatch=1
      fi
      m=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
      [ -z "$m" ] && continue
      # Rank: cwd-match first, then mtime
      if [ "$cwdMatch" -gt "$bestCwdMatch" ] || { [ "$cwdMatch" -eq "$bestCwdMatch" ] && [ "$m" -gt "$bestMtime" ]; }; then
        bestCwdMatch="$cwdMatch"
        bestMtime="$m"
        bestUuid="$uuid"
      fi
    done
  fi

  # Classify
  local state="unknown"
  if [ -n "$bestUuid" ]; then
    local now age
    now=$(date +%s)
    age=$((now - bestMtime))
    if [ -n "$pid" ] && [ "$age" -lt 120 ]; then
      state="live"
    elif [ -n "$pid" ]; then
      state="stable"
    else
      state="stale"
    fi
  else
    # No JSONL found — if sessions.env has an entry for this pane, it's broken
    local ses="${HIVEMIND_SESSIONS:-${CONFIG_PATH:-$HOME/config}/hivemind.sessions.env}"
    if [ -f "$ses" ]; then
      local cachedUuid
      cachedUuid=$(grep "^${pane}|" "$ses" 2>/dev/null | head -1 | cut -d'|' -f2)
      if [ -n "$cachedUuid" ]; then
        bestUuid="$cachedUuid"
        state="broken"
      fi
    fi
  fi

  echo "${bestUuid}|${state}|${cleanTitle}"
  [ -n "$bestUuid" ] && return 0 || return 1
}

claudeCode.session.current() # <pane> # print current session UUID (non-invasive); rc=1 when unresolvable
{
  local out
  out=$(private.claudeCode.session.discover "$1")
  local uuid="${out%%|*}"
  [ -z "$uuid" ] && return 1
  echo "$uuid"
  return 0
}
claudeCode.session.current.completion.pane() { private.claudeCode.complete.panes; }

claudeCode.session.state() # <pane> # print session state: live|stable|stale|broken|unknown
{
  local out field2
  out=$(private.claudeCode.session.discover "$1")
  field2="${out#*|}"
  echo "${field2%%|*}"
}
claudeCode.session.state.completion.pane() { private.claudeCode.complete.panes; }

claudeCode.session.probe.fromCapture() # <captureText> # pure parser: extract UUID from /status output
# Pure Model — no otmux, no tmux, no side effects. Takes captured TUI text via arg
# or stdin, returns the session UUID. Testable with fixture strings.
#
# Parses two /status formats:
#   1. Legacy (Claude Code < 2.1.x): "Session ID: <uuid>"
#   2. Current (Claude Code 2.1.x+): "Session name: <customTitle>" — correlated
#      to the newest JSONL in ~/.claude/projects/*/*.jsonl with matching customTitle
#      (handles forks + /rename history).
{
  local capture="$1"
  [ -z "$capture" ] && capture=$(cat)
  [ -z "$capture" ] && return 1

  # 1. Legacy "Session ID:" — direct UUID extraction
  local sid
  sid=$(echo "$capture" | grep -oE 'Session ID: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
  [ -n "$sid" ] && echo "$sid" && return 0

  # 2. "Session name:" — correlate to newest JSONL with matching customTitle
  local name
  name=$(echo "$capture" | grep -oE 'Session name:[[:space:]]+[^[:space:]].*' | head -1 | sed -E 's/^Session name:[[:space:]]+//;s/[[:space:]]+$//')
  [ -z "$name" ] && return 1

  local newest="" newest_m=0
  local f m
  for f in "$HOME/.claude/projects"/*/*.jsonl; do
    [ -f "$f" ] || continue
    grep -q "\"customTitle\":\"${name}\"" "$f" || continue
    m=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
    [ -z "$m" ] && continue
    if [ "$m" -gt "$newest_m" ]; then
      newest_m="$m"
      newest="$f"
    fi
  done
  [ -z "$newest" ] && return 1
  basename "$newest" .jsonl
  return 0
}

# claudeCode.session.probe REMOVED in A1.2 Fix #2b — Model purity restored.
# View I/O (otmux send/capture) now lives in hiveMind.agent.session.probe (Controller).
# The Model retains only the pure parser claudeCode.session.probe.fromCapture above.
# Migrate callers: claudeCode.session.probe <pane>  →  hiveMind agent.session.probe <pane>


claudeCode.session.id() # <pane> # get Claude Code session UUID for a tmux pane (cache-first)
{
  local target="$1"
  [ -z "$target" ] && return 1

  # Cache: sessions.env (kept fresh by hiveMind registry.refresh on every
  # lifecycle edge: agent.rename/spawn/bootstrap/respawn/restart/team.restart)
  local ses="${HIVEMIND_SESSIONS:-${CONFIG_PATH:-$HOME/config}/hivemind.sessions.env}"
  if [ -f "$ses" ]; then
    local sid
    sid=$(grep "^${target}|" "$ses" 2>/dev/null | tail -1 | cut -d'|' -f2)
    [ -n "$sid" ] && echo "$sid" && return 0
  fi

  # Authoritative fallback: session.current (JSONL customTitle correlation,
  # non-invasive, handles forks + autocompact uniformly).
  claudeCode.session.current "$target"
}
claudeCode.session.id.completion.pane() {
  private.claudeCode.complete.panes
}

claudeCode.session.name() # <sessionId> # get session name by UUID (customTitle from /rename, or firstPrompt)
{
  local sid="$1"
  [ -z "$sid" ] && return 1

  local CLAUDE_PROJECTS_DIR="$HOME/.claude/projects"
  local name=""

  # Method 1: check JSONL for custom-title entry (from /rename — most recent, survives forks)
  for projectDir in "$CLAUDE_PROJECTS_DIR"/*/; do
    local jsonlFile="${projectDir}${sid}.jsonl"
    [ -f "$jsonlFile" ] || continue
    local raw_line
    raw_line=$(grep '"custom-title"' "$jsonlFile" 2>/dev/null | tail -1)
    if [ -n "$raw_line" ]; then
      if command -v jq >/dev/null 2>&1; then
        name=$(echo "$raw_line" | jq -r '.customTitle // empty' 2>/dev/null | head -1)
      elif command -v python3 >/dev/null 2>&1; then
        name=$(echo "$raw_line" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('customTitle',''))" 2>/dev/null)
      fi
    fi
    name="${name%%$'\n'*}"
    [ -n "$name" ] && break
  done

  if [ -n "$name" ]; then
    echo "$name"
    return 0
  fi

  # Method 2: look up in sessions-index.json (fallback — may have inherited title from fork parent)
  for indexFile in "$CLAUDE_PROJECTS_DIR"/*/sessions-index.json; do
    [ -f "$indexFile" ] || continue
    if command -v jq >/dev/null 2>&1; then
      name=$(jq -r --arg sid "$sid" '
        .entries[] | select(.sessionId == $sid) |
        if .customTitle and .customTitle != "" then .customTitle
        elif .firstPrompt and .firstPrompt != "" then .firstPrompt[0:40]
        else empty end
      ' "$indexFile" 2>/dev/null)
    elif command -v python3 >/dev/null 2>&1; then
      name=$(python3 -c "
import json, sys
try:
    d = json.load(open('$indexFile'))
    for e in d.get('entries', []):
        if e.get('sessionId') == '$sid':
            t = e.get('customTitle') or ''
            if t: print(t); sys.exit()
            p = e.get('firstPrompt') or ''
            if p: print(p[:40]); sys.exit()
except: pass
" 2>/dev/null)
    fi
    [ -n "$name" ] && break
  done

  if [ -n "$name" ]; then
    echo "$name"
    return 0
  fi

  # Method 3: read firstPrompt directly from JSONL file (for sessions not yet in index)
  for projectDir in "$CLAUDE_PROJECTS_DIR"/*/; do
    local jsonlFile="${projectDir}${sid}.jsonl"
    [ -f "$jsonlFile" ] || continue
    name=$(head -20 "$jsonlFile" \
      | jq -r 'select(.type == "user") | .message.content // empty' 2>/dev/null \
      | sed 's/<[^>]*>//g; s/^[[:space:]]*//; /^$/d' \
      | grep -vi '^caveat:' \
      | grep -v '^/clear' \
      | grep -vi '^clear$' \
      | head -1)
    [ -n "$name" ] && name="${name:0:40}" && break
  done

  if [ -n "$name" ]; then
    echo "$name"
    return 0
  fi

  return 1
}
claudeCode.session.name.completion.sessionId() {
  private.claudeCode.complete.sessionIds
}

claudeCode.agent.start() # <?workdir:.> <?model:sonnet> # start Claude Code with optional model
{
  local workdir="$(pwd)"
  local model=""

  # Parse arguments
  while [ $# -gt 0 ]; do
    case "$1" in
      --model) model="$2"; shift 2 ;;
      *) workdir="$1"; shift ;;
    esac
  done

  local cmd="$CLAUDE_CMD"
  [ -n "$model" ] && cmd="$cmd --model $model"

  if [ -n "$TMUX" ]; then
    local currentPane
    currentPane=$(otmux pane.get)
    "$OOSH_DIR/otmux" send "$currentPane" "cd '$workdir' && $cmd" Enter
    info.log "Started Claude Code in current pane${model:+ with model $model}"
  else
    cd "$workdir" && eval "$cmd"
  fi
}

claudeCode.session.save() # <?file:session/agent.context.md> # save current session context to file
{
  local file="${1:-session/agent.context.md}"
  local dir
  dir=$(dirname "$file")

  if [ ! -d "$dir" ]; then
    mkdir -p "$dir"
  fi

  if [ ! -f "$file" ]; then
    # Create template
    cat > "$file" << 'TEMPLATE'
# Agent Context State

**Session**: unknown
**Updated**: $(date -u +%Y-%m-%dT%H:%MZ)
**Role**: unknown

## Current Task
(no task set)

## Team Status
| Pane | Agent | Role | Status |
|------|-------|------|--------|

## Recovery
1. Read this file
2. Read current task file
3. Check agent panes with `otmux pane.capture`
4. Resume work
TEMPLATE
    info.log "Created context template: $file"
  else
    # Update timestamp
    if command -v sed &>/dev/null; then
      local timestamp
      timestamp=$(date -u +%Y-%m-%dT%H:%MZ)
      sed -i '' "s/^\*\*Updated\*\*:.*/\*\*Updated\*\*: $timestamp/" "$file" 2>/dev/null
      info.log "Updated timestamp in: $file"
    fi
  fi

  success.log "Context file ready: $file"
  RESULT="$file"
}

claudeCode.session.recover() # <?file:session/agent.context.md> # read and display recovery context
{
  local file="${1:-session/agent.context.md}"

  if [ ! -f "$file" ]; then
    error.log "Context file not found: $file"
    echo "  Create one with: claudeCode session.save"
    return 1
  fi

  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║              Session Recovery Context                      ║"
  echo "╠════════════════════════════════════════════════════════════╣"
  cat "$file"
  echo ""
  echo "╚════════════════════════════════════════════════════════════╝"

  RESULT="$file"
}

claudeCode.session.recover.completion.file() {
  ls session/*.md 2>/dev/null
}

claudeCode.status() # # show current claude status (no TUI launch)
{
  echo "Claude Code Status"
  echo "=================="

  # Version
  local ver
  ver=$("$CLAUDE_CMD" --version 2>/dev/null | head -1)
  echo "  Version:  ${ver:-unknown}"

  # Binary location
  echo "  Binary:   $CLAUDE_CMD"

  # HIVEMIND_ROLE if set
  if [ -n "${HIVEMIND_ROLE:-}" ]; then
    echo "  Role:     $HIVEMIND_ROLE"
  fi

  # Current pane's Claude process
  if [ -n "$TMUX" ]; then
    local paneTarget
    paneTarget=$(otmux pane.get.target 2>/dev/null)
    if [ -n "$paneTarget" ]; then
      local pid
      pid=$(claudeCode.process.find "$paneTarget" 2>/dev/null)
      if [ -n "$pid" ]; then
        echo "  PID:      $pid (pane $paneTarget)"
        local sid
        sid=$(claudeCode.session.id "$paneTarget" 2>/dev/null)
        [ -n "$sid" ] && echo "  Session:  $sid"
      else
        echo "  Process:  not running in $paneTarget"
      fi
    fi
  fi

  echo ""
}

# ─────────────────────────────────────────────────────────────────────────────
# CONTEXT MONITORING (peer observation)
# ─────────────────────────────────────────────────────────────────────────────

private.claudeCode.context.parse() {
  # Parse context percentage from captured pane content
  # Claude Code TUI shows: "Context left until auto-compact: NN%"
  local content="$1"
  local pct

  # Strip ANSI escape codes first
  local clean
  clean=$(echo "$content" | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\x1b\[[0-9;]*[A-Za-z]//g')

  # Pattern 1: "Context left until auto-compact: NN%"
  pct=$(echo "$clean" | grep -oE 'Context left until auto-compact: [0-9]+%' | grep -oE '[0-9]+' | head -1)
  [ -n "$pct" ] && echo "$pct" && return 0

  # Pattern 2: "context: NN%" or "Context: NN%"
  pct=$(echo "$clean" | grep -oiE 'context:\s*[0-9]+%' | grep -oE '[0-9]+' | head -1)
  [ -n "$pct" ] && echo "$pct" && return 0

  # Pattern 3: Any line with "context" and a percentage nearby
  pct=$(echo "$clean" | grep -iE 'context.*[0-9]+%' | grep -oE '[0-9]+' | head -1)
  [ -n "$pct" ] && echo "$pct" && return 0

  # Pattern 4: Look for percentage near bottom of pane (status bar area)
  pct=$(echo "$clean" | tail -10 | grep -oE '[0-9]+%' | grep -oE '[0-9]+' | head -1)
  [ -n "$pct" ] && echo "$pct" && return 0

  # Debug: log what we captured if parsing fails
  debug.log "context.parse failed. First 5 lines: $(echo "$clean" | head -5)"
  debug.log "context.parse failed. Last 5 lines: $(echo "$clean" | tail -5)"

  return 1
}

claudeCode.context.jsonl() # <?pane> # find .jsonl file for a pane (per-pane), or most recent if no pane
{
  local pane="$1"

  # Per-pane resolution: pane -> session UUID -> JSONL file
  if [ -n "$pane" ]; then
    local sid
    sid=$(claudeCode.session.id "$pane" 2>/dev/null)
    if [ -n "$sid" ]; then
      for projectDir in "$HOME/.claude/projects"/*/; do
        local jsonlFile="${projectDir}${sid}.jsonl"
        if [ -f "$jsonlFile" ]; then
          echo "$jsonlFile"
          return 0
        fi
      done
    fi
  fi

  # Fallback: most recently modified JSONL (for no-pane usage)
  local projectDirs=(
    "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude-components-OOSH-dev-claude"
    "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude"
  )

  local newestFile=""
  local newestTime=0

  for dir in "${projectDirs[@]}"; do
    [ -d "$dir" ] || continue
    while IFS= read -r file; do
      [ -f "$file" ] || continue
      local mtime
      mtime=$(stat -f %m "$file" 2>/dev/null) || continue
      if [ "$mtime" -gt "$newestTime" ]; then
        newestTime="$mtime"
        newestFile="$file"
      fi
    done < <(find "$dir" -maxdepth 1 -name "*.jsonl" 2>/dev/null)
  done

  if [ -n "$newestFile" ]; then
    echo "$newestFile"
    return 0
  fi

  return 1
}
claudeCode.context.jsonl.completion() { :; }

private.claudeCode.max.tokens.for.jsonl() # <jsonlFile> # detect per-session context limit
{
  # Detection priority (see CLAUDE_MAX_TOKENS_* constants at top of file):
  # 1. ps args — running claude process for this sessionId with "[1m]" flag → 1M.
  # 2. Observed max — any JSONL usage > default → must be 1M (default-capped
  #    sessions would have compacted before exceeding their limit).
  # 3. Fall back to default (opus/sonnet/haiku base window).
  local jsonlFile="$1"
  [ -f "$jsonlFile" ] || { echo "$CLAUDE_MAX_TOKENS_DEFAULT"; return 0; }

  local sid
  sid=$(basename "$jsonlFile" .jsonl)

  # 1. Live ps-args check — is this session running with [1m]?
  if command -v ps &>/dev/null && [ -n "$sid" ]; then
    if ps -eo args= 2>/dev/null | grep -F "$sid" | grep -q '\[1m\]'; then
      echo "$CLAUDE_MAX_TOKENS_1M"
      return 0
    fi
  fi

  # 2. Observed-max auto-detect — any usage exceeding default means it's 1M
  local observed_max
  observed_max=$(tail -200 "$jsonlFile" 2>/dev/null | python3 -c "
import sys, json
m = 0
for line in sys.stdin:
    try:
        d = json.loads(line)
        if d.get('type') == 'assistant':
            u = d.get('message', {}).get('usage', {})
            if u and 'input_tokens' in u:
                t = u['input_tokens'] + u.get('cache_creation_input_tokens', 0) + u.get('cache_read_input_tokens', 0)
                if t > m: m = t
    except: pass
print(m)
" 2>/dev/null)
  if [ -n "$observed_max" ] && [ "$observed_max" -gt "$CLAUDE_MAX_TOKENS_DEFAULT" ] 2>/dev/null; then
    echo "$CLAUDE_MAX_TOKENS_1M"
    return 0
  fi

  # 3. Default (opus/sonnet/haiku base window)
  echo "$CLAUDE_MAX_TOKENS_DEFAULT"
}

private.claudeCode.context.from.jsonl() {
  # Read context percentage from JSONL token data
  local jsonlFile="$1"
  [ -f "$jsonlFile" ] || return 1

  # Staleness guard: if JSONL hasn't been written in >10 min, it's likely
  # from a dead/compacted session — return "stale" so caller falls through
  local mtime now age
  mtime=$(stat -f %m "$jsonlFile" 2>/dev/null || stat -c %Y "$jsonlFile" 2>/dev/null)
  now=$(date +%s)
  if [ -n "$mtime" ]; then
    age=$(( now - mtime ))
    if [ "$age" -gt 600 ]; then
      echo "stale"
      return 1
    fi
  fi

  local maxTokens
  maxTokens=$(private.claudeCode.max.tokens.for.jsonl "$jsonlFile")

  tail -50 "$jsonlFile" | MAX_TOKENS="$maxTokens" python3 -c "
import sys, json, os
max_tokens = int(os.environ.get('MAX_TOKENS') or os.environ.get('CLAUDE_MAX_TOKENS_DEFAULT') or 200000)
last_usage = None
for line in sys.stdin:
    try:
        d = json.loads(line)
        if d.get('type') == 'assistant':
            usage = d.get('message', {}).get('usage', {})
            if usage and 'input_tokens' in usage:
                last_usage = usage
    except: pass
if last_usage:
    # Total context = input_tokens + cache_creation + cache_read
    # input_tokens: non-cached prompt portion (often just 1 with full caching)
    # cache_creation: new tokens being written to cache this turn
    # cache_read: previously cached tokens read from cache
    # Together = full prompt sent to model = context window usage
    input_t = last_usage['input_tokens']
    cache_create = last_usage.get('cache_creation_input_tokens', 0)
    cache_read = last_usage.get('cache_read_input_tokens', 0)
    total = input_t + cache_create + cache_read
    pct = round((total / max_tokens) * 100, 1)
    remaining = round(100 - pct, 1)
    print(f'{remaining}')
else:
    print('unknown')
" 2>/dev/null
}

claudeCode.context.self() # # read own context % using TMUX_PANE — rc=1 if not in tmux
{
  if [ -z "$TMUX" ] && [ -z "$TMUX_PANE" ]; then
    error.log "Not in a tmux pane (TMUX and TMUX_PANE both unset)"
    return 1
  fi
  local myPane
  myPane=$(otmux pane.get.target 2>/dev/null)
  if [ -z "$myPane" ]; then
    error.log "Not in a tmux pane"
    return 1
  fi
  claudeCode.context.read "$myPane"
}

claudeCode.context.read() # <?pane> # read context % remaining from JSONL token data (falls back to TUI)
{
  local pane="$1"

  # Guard: if pane specified, verify Claude is actually running there
  if [ -n "$pane" ]; then
    if ! claudeCode.process.running "$pane" 2>/dev/null; then
      RESULT="no-claude"
      echo "no-claude"
      return 1
    fi
  fi

  # Try JSONL first (most reliable)
  local jsonlFile
  if [ -n "$pane" ]; then
    # Pane-aware: map pane → session ID → specific JSONL file
    local sid
    sid=$(claudeCode.session.id "$pane" 2>/dev/null)
    if [ -n "$sid" ]; then
      # Search ALL project dirs for this session's JSONL (not hardcoded)
      local dir
      for dir in "$HOME/.claude/projects"/*/; do
        if [ -f "${dir}${sid}.jsonl" ]; then
          jsonlFile="${dir}${sid}.jsonl"
          break
        fi
      done
    fi
  fi
  # Fallback: global newest ONLY if no pane specified
  # If pane was specified but session lookup failed, don't use global (wrong agent's data)
  if [ -z "$jsonlFile" ] && [ -z "$pane" ]; then
    jsonlFile=$(claudeCode.context.jsonl)
  fi
  if [ -n "$jsonlFile" ] && [ -f "$jsonlFile" ]; then
    local pct
    pct=$(private.claudeCode.context.from.jsonl "$jsonlFile")
    if [ -n "$pct" ] && [ "$pct" != "unknown" ] && [ "$pct" != "stale" ]; then
      RESULT="$pct"
      echo "$pct"
      return 0
    fi
    # JSONL stale or unreadable — try session.current for a fresher UUID
    if [ "$pct" = "stale" ] && [ -n "$pane" ]; then
      local freshSid
      freshSid=$(claudeCode.session.current "$pane" 2>/dev/null)
      if [ -n "$freshSid" ] && [ "$freshSid" != "$sid" ]; then
        local freshJsonl=""
        for dir in "$HOME/.claude/projects"/*/; do
          [ -f "${dir}${freshSid}.jsonl" ] && freshJsonl="${dir}${freshSid}.jsonl" && break
        done
        if [ -n "$freshJsonl" ]; then
          pct=$(private.claudeCode.context.from.jsonl "$freshJsonl")
          if [ -n "$pct" ] && [ "$pct" != "unknown" ] && [ "$pct" != "stale" ]; then
            RESULT="$pct"
            echo "$pct"
            return 0
          fi
        fi
      fi
    fi
  fi

  # Fall back to TUI scraping if pane specified
  if [ -n "$pane" ]; then
    private.claudeCode.context.read.tui "$pane"
    return $?
  fi

  RESULT="unknown"
  echo "unknown"
  return 1
}
claudeCode.context.read.completion.pane() {
  private.claudeCode.complete.panes
}

private.claudeCode.context.read.tui() # <pane> # read context percentage from Claude Code TUI status bar
{
  local pane="$1"
  if [ -z "$pane" ]; then
    error.log "Usage: claudeCode context.read.tui <pane>"
    return 1
  fi

  # Capture FULL pane content — context info can be anywhere
  local content
  content=$(otmux pane.capture "$pane" 200 2>/dev/null)

  # Also check visible area (bottom of pane where status bar lives)
  local visible
  visible=$(otmux pane.capture.visible "$pane" 2>/dev/null)

  # Try parsing full content first
  local pct
  pct=$(private.claudeCode.context.parse "$content")
  if [ -n "$pct" ]; then
    RESULT="$pct"
    echo "$pct"
    return 0
  fi

  # Try visible area (may have fresher status bar)
  pct=$(private.claudeCode.context.parse "$visible")
  if [ -n "$pct" ]; then
    RESULT="$pct"
    echo "$pct"
    return 0
  fi

  RESULT="unknown"
  echo "unknown"
  return 1
}
private.claudeCode.context.read.tui.completion.pane() {
  private.claudeCode.complete.panes
}

claudeCode.context.all() # # show context % for all active sessions
{
  local projectDirs=(
    "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude-components-OOSH-dev-claude"
    "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude"
  )

  local now
  now=$(date +%s)
  local maxAge=3600  # Only show sessions modified in last hour

  for dir in "${projectDirs[@]}"; do
    [ -d "$dir" ] || continue
    while IFS= read -r file; do
      [ -f "$file" ] || continue
      local mtime
      mtime=$(stat -f %m "$file" 2>/dev/null) || continue
      local age=$((now - mtime))
      [ "$age" -gt "$maxAge" ] && continue

      local uuid
      uuid=$(basename "$file" .jsonl)
      local pct
      pct=$(private.claudeCode.context.from.jsonl "$file")
      local minsAgo=$((age / 60))
      echo "${uuid:0:8}... ${pct}% (${minsAgo}m ago)"
    done < <(find "$dir" -maxdepth 1 -name "*.jsonl" 2>/dev/null)
  done
}
claudeCode.context.all.completion() { :; }

claudeCode.context.velocity() # <?paneOrJsonl> # calculate context burn rate — dispatches to byPane or byJsonl
{
  local arg="$1"
  if [ -z "$arg" ]; then
    # No args: use global newest jsonl
    local jsonlFile
    jsonlFile=$(claudeCode.context.jsonl)
    [ -z "$jsonlFile" ] || [ ! -f "$jsonlFile" ] && { echo "unknown"; return 1; }
    private.claudeCode.velocity.calculate "$jsonlFile"
  elif [ -f "$arg" ]; then
    claudeCode.context.velocity.byJsonl "$arg"
  else
    claudeCode.context.velocity.byPane "$arg"
  fi
}
claudeCode.context.velocity.completion.paneOrJsonl() {
  private.claudeCode.complete.panes
}

claudeCode.context.velocity.byPane() # <pane> # calculate velocity by resolving pane → session → jsonl
{
  local pane="$1"
  [ -z "$pane" ] && { error.log "usage: claudeCode context.velocity.byPane <pane>"; return 1; }
  local sid
  sid=$(claudeCode.session.id "$pane" 2>/dev/null)
  if [ -n "$sid" ]; then
    local projectDirs=(
      "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude-components-OOSH-dev-claude"
      "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude"
    )
    local jsonlFile=""
    for dir in "${projectDirs[@]}"; do
      if [ -f "$dir/$sid.jsonl" ]; then
        jsonlFile="$dir/$sid.jsonl"
        break
      fi
    done
    [ -z "$jsonlFile" ] && { echo "unknown"; return 1; }
    private.claudeCode.velocity.calculate "$jsonlFile"
  else
    echo "unknown"
    return 1
  fi
}
claudeCode.context.velocity.byPane.completion.pane() {
  private.claudeCode.complete.panes
}

claudeCode.context.velocity.byJsonl() # <jsonl> # calculate velocity from a specific jsonl file
{
  local jsonlFile="$1"
  [ -z "$jsonlFile" ] || [ ! -f "$jsonlFile" ] && { echo "unknown"; return 1; }
  private.claudeCode.velocity.calculate "$jsonlFile"
}
claudeCode.context.velocity.byJsonl.completion.jsonl() {
  ls "$HOME/.claude/projects"/*/*.jsonl 2>/dev/null
}

private.claudeCode.velocity.calculate() # <jsonlFile> # parse velocity from jsonl
{
  local jsonlFile="$1"
  [ -z "$jsonlFile" ] || [ ! -f "$jsonlFile" ] && { echo "unknown"; return 1; }

  local maxTokens
  maxTokens=$(private.claudeCode.max.tokens.for.jsonl "$jsonlFile")

  MAX_TOKENS="$maxTokens" python3 -c "
import json, sys, os
from datetime import datetime

max_tokens = int(os.environ.get('MAX_TOKENS') or os.environ.get('CLAUDE_MAX_TOKENS_DEFAULT') or 200000)

messages = []
with open('$jsonlFile') as f:
    for line in f:
        try:
            d = json.loads(line)
            if d.get('type') == 'assistant':
                msg = d.get('message', {})
                usage = msg.get('usage', {})
                if usage and 'input_tokens' in usage:
                    ts = d.get('timestamp', '')
                    total = usage['input_tokens'] + usage.get('cache_creation_input_tokens', 0) + usage.get('cache_read_input_tokens', 0)
                    messages.append({'ts': ts, 'tokens': total})
        except:
            pass

if len(messages) < 2:
    print('unknown')
    sys.exit(0)

first = messages[0]
last = messages[-1]

try:
    t1 = datetime.fromisoformat(first['ts'].replace('Z', '+00:00'))
    t2 = datetime.fromisoformat(last['ts'].replace('Z', '+00:00'))
    hours = (t2 - t1).total_seconds() / 3600
    if hours <= 0:
        print('unknown')
        sys.exit(0)

    token_growth = last['tokens'] - first['tokens']
    tokens_per_hour = token_growth / hours

    current = last['tokens']
    threshold = int(max_tokens * int(os.environ.get('CLAUDE_COMPACT_THRESHOLD_PCT') or 90) / 100)
    remaining = threshold - current

    if tokens_per_hour > 0 and remaining > 0:
        hours_left = remaining / tokens_per_hour
        mins_left = int(hours_left * 60)
        print(f'{int(tokens_per_hour)} tokens/hr | {current} current | ~{mins_left}min until compact')
    elif remaining <= 0:
        print(f'{int(tokens_per_hour)} tokens/hr | {current} current | COMPACT NOW')
    else:
        print(f'{int(tokens_per_hour)} tokens/hr | {current} current')
except Exception as e:
    print('unknown')
" 2>/dev/null

  return 0
}

claudeCode.context.dashboard() # # show velocity dashboard for all active sessions
{
  local projectDirs=(
    "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude-components-OOSH-dev-claude"
    "$HOME/.claude/projects/-Users-Shared-Workspaces-AI-Claude"
  )

  echo "=== Context Velocity Dashboard ==="
  echo "Session  | Usage | Rate    | Time Left"
  echo "---------|-------|---------|----------"

  local now
  now=$(date +%s)

  for dir in "${projectDirs[@]}"; do
    [ -d "$dir" ] || continue
    while IFS= read -r latest; do
      [ -f "$latest" ] || continue

      # Only show recently modified (last hour)
      local mtime
      mtime=$(stat -f %m "$latest" 2>/dev/null) || continue
      local age=$((now - mtime))
      [ "$age" -gt 3600 ] && continue

      local uuid
      uuid=$(basename "$latest" .jsonl)
      local short="${uuid:0:8}"

      # Get velocity info (per-session max_tokens aware)
      local maxTokens
      maxTokens=$(private.claudeCode.max.tokens.for.jsonl "$latest")
      local info
      info=$(JSONL_FILE="$latest" MAX_TOKENS="$maxTokens" python3 -c "
import json, sys, os
from datetime import datetime

jsonl_file = os.environ.get('JSONL_FILE', '')
max_tokens = int(os.environ.get('MAX_TOKENS') or os.environ.get('CLAUDE_MAX_TOKENS_DEFAULT') or 200000)
threshold = int(max_tokens * int(os.environ.get('CLAUDE_COMPACT_THRESHOLD_PCT') or 90) / 100)

messages = []
with open(jsonl_file) as f:
    for line in f:
        try:
            d = json.loads(line)
            if d.get('type') == 'assistant':
                usage = d.get('message', {}).get('usage', {})
                if usage and 'input_tokens' in usage:
                    total = usage['input_tokens'] + usage.get('cache_creation_input_tokens', 0) + usage.get('cache_read_input_tokens', 0)
                    messages.append({'ts': d.get('timestamp', ''), 'tokens': total})
        except: pass

if len(messages) < 2:
    print('- | - | -')
    sys.exit(0)

first, last = messages[0], messages[-1]
try:
    t1 = datetime.fromisoformat(first['ts'].replace('Z', '+00:00'))
    t2 = datetime.fromisoformat(last['ts'].replace('Z', '+00:00'))
    hours = (t2 - t1).total_seconds() / 3600
    if hours <= 0:
        print(f\"{round(last['tokens']/max_tokens*100,1)}% | - | -\")
        sys.exit(0)
    rate = int((last['tokens'] - first['tokens']) / hours)
    remaining = threshold - last['tokens']
    mins = int(remaining / rate * 60) if rate > 0 and remaining > 0 else 0
    pct = round(last['tokens'] / max_tokens * 100, 1)
    status = 'COMPACT!' if remaining <= 0 else f'{mins}min'
    print(f\"{pct}% | {rate}/hr | {status}\")
except:
    print('- | - | -')
" 2>/dev/null)

      echo "$short | $info"
    done < <(find "$dir" -maxdepth 1 -name "*.jsonl" 2>/dev/null)
  done
}
claudeCode.context.dashboard.completion() { :; }

claudeCode.context.check() # <pane> # check context health: percentage, velocity, log, alert if low
{
  local pane="$1"

  if [ -z "$pane" ]; then
    error.log "Usage: claudeCode context.check <pane>"
    return 1
  fi

  # 1. Get context percentage
  local pct
  pct=$(claudeCode.context.read "$pane" 2>/dev/null)
  [ -z "$pct" ] && pct="unknown"

  # 2. Get velocity
  local velocity ttc tokensHr
  velocity=$(claudeCode.context.velocity.byPane "$pane" 2>/dev/null)
  tokensHr=$(echo "$velocity" | grep -oE '^[0-9]+ tokens/hr' || echo "-")
  ttc=$(echo "$velocity" | grep -oE '~[0-9]+min' || echo "-")

  # 3. Determine state
  local state="ok"
  if [ "$pct" = "unknown" ]; then
    state="unknown"
  elif [ "$pct" -le 10 ] 2>/dev/null; then
    state="CRITICAL"
  elif [ "$pct" -le 25 ] 2>/dev/null; then
    state="LOW"
  fi

  # 4. Log to burn log
  local workspace
  workspace="$(git rev-parse --show-toplevel 2>/dev/null)"
  if [ -n "$workspace" ]; then
    local logfile="${workspace}/session/context-burn-log.md"
    local ts
    ts=$(date '+%H:%M')
    if [ ! -f "$logfile" ]; then
      echo "| Time | Pane | % | State | Velocity | TTC |" > "$logfile"
      echo "|------|------|---|-------|----------|-----|" >> "$logfile"
    fi
    echo "| $ts | $pane | ${pct}% | $state | $tokensHr | $ttc |" >> "$logfile"
  fi

  # 5. Alert if low
  if [ "$state" = "LOW" ]; then
    warn.log "$pane: ${pct}% context — alert peer"
  elif [ "$state" = "CRITICAL" ]; then
    warn.log "$pane: ${pct}% context — CRITICAL, triggering compact"
    claudeCode.context.alert "$pane" 10
  fi

  # 6. Output summary
  console.log "$pane: ${pct}% | $tokensHr | $ttc | $state"
  create.result 0 "$pct"
  return $(result)
}
claudeCode.context.check.completion.pane() {
  private.claudeCode.complete.panes
}

claudeCode.context.alert() # <pane> <?threshold:20> # alert if context below threshold; sends warning to pane
{
  local pane="$1"
  local threshold="${2:-20}"

  if [ -z "$pane" ]; then
    error.log "Usage: claudeCode context.alert <pane> <?threshold>"
    return 1
  fi

  claudeCode.context.read "$pane" > /dev/null
  local pct="$RESULT"

  # Can't alert if context is unknown
  if [ "$pct" = "unknown" ]; then
    debug.log "Cannot alert $pane — context unknown"
    return 1
  fi

  if [ "$pct" -le "$threshold" ] 2>/dev/null; then
    warn.log "Agent at $pane has ${pct}% context remaining"
    "$OOSH_DIR/otmux" send.enter "$pane" "CONTEXT: ${pct}% — save state now"
    return 0
  fi

  return 1
}
claudeCode.context.alert.completion.pane() {
  private.claudeCode.complete.panes
}

# claudeCode.agent.recover was deleted in A1.2 fix 3 (duplicated hiveMind.agent.unblock
# family). Controller equivalents:
#   - hiveMind.agent.unblock <agentName>  — detect + resolve stuck prompts
#   - hiveMind.agent.handoff <agentName>  — full generational handoff with verify
# See task-a1.2-expert-view-leak-identification.md Leak 2.

claudeCode.usage()
{
  local this=${0##*/}
  echo "You started"
  echo "$0

  claudeCode - Claude Code wrapper for oosh
  Makes cryptic --flags easy to remember!

  Usage:
  $this: command     Description
  ─────────────────────────────────────────────────────
  SESSION MANAGEMENT:
      list          show session picker (--resume)
      join <name>   resume specific session (--resume <name>)
      join.byID     resume by session UUID
      join.byName   resume by role name (sessions.env lookup)
      join.byPane   resume by resolving pane → UUID
      fork <uuid>   fork session with new ID (cross-machine)
      continue      continue most recent session (--continue)
      new           start fresh conversation

  OUTPUT MODES:
      print <msg>   non-interactive output (--print)
      json <msg>    output as JSON
      pipe          read from stdin (-p)

  MODELS:
      opus <msg>    use opus model
      sonnet <msg>  use sonnet model
      haiku <msg>   use haiku model
      model <m>     specify model (--model)

  PERMISSIONS:
      yolo <msg>    skip all prompts (--dangerously-skip-permissions)
      dangerously   same as yolo

  CONFIGURATION:
      config        open settings
      doctor        check setup
      init          initialize directory
      mcp           manage MCP servers

  INSTALLATION:
      install       install claude code from web for linux
      uninstall     remove claude code installation
      update        update claude code to latest version
      login         authenticate with claude
      logout        log out from claude

  AGENT SUPPORT:
      agent.start       start claude code in pane
      session.save      save context to file
      session.recover   read recovery context
      process.find      find Claude PID for a tmux pane
      process.running   check if Claude is running in pane
      session.id        get session UUID for a tmux pane

  CONTEXT MONITORING:
      context.self            read own context % (auto-detects pane)
      context.read <pane>     read context % from TUI status bar
      context.alert <pane>    alert if context below threshold (default 20%)
      context.velocity        token burn rate (dispatches to byPane/byJsonl)
      context.velocity.byPane   velocity by resolving pane → session → jsonl
      context.velocity.byJsonl  velocity from a specific jsonl file

  OTHER:
      verbose       verbose output
      maxTurns <n>  limit turns
      help          show claude --help
      v             show version
  ─────────────────────────────────────────────────────

  Examples:
    $this list                    # pick a session to resume
    $this continue                # continue last session
    $this join my-feature         # resume 'my-feature' session
    $this opus 'explain this'     # use opus model
    $this print 'quick question'  # get answer and exit
    $this yolo 'fix all tests'    # no permission prompts
    $this session.save            # save agent context
    $this session.recover         # read recovery context
    cat file.py | $this pipe 'review this'
  "
}

claudeCode.start()
{
  #echo "sourcing init"
  source this

  if [ -z "$1" ]; then
    claudeCode.usage
    return 0
  fi

  this.start "$@"
}

claudeCode.start "$@"
