#!/usr/bin/env bash
# humanize.sh - Humanize shell utilities
# Part of rc.d configuration
# Compatible with both bash and zsh

# Source shared monitor utilities (per plan: scripts/lib/monitor-common.sh)
HUMANIZE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
if [[ -f "$HUMANIZE_SCRIPT_DIR/lib/monitor-common.sh" ]]; then
    source "$HUMANIZE_SCRIPT_DIR/lib/monitor-common.sh"
fi

# Source shared loop library (provides DEFAULT_CODEX_MODEL and other constants)
HUMANIZE_HOOKS_LIB_DIR="$(cd "$HUMANIZE_SCRIPT_DIR/../hooks/lib" && pwd)"
if [[ -f "$HUMANIZE_HOOKS_LIB_DIR/loop-common.sh" ]]; then
    source "$HUMANIZE_HOOKS_LIB_DIR/loop-common.sh"
fi

# ========================================
# Public helper functions (can be called directly for testing)
# ========================================

# Split pipe-delimited string into array (bash/zsh compatible)
# Usage: humanize_split_to_array "output_array_name" "value1|value2|value3"
humanize_split_to_array() {
    local arr_name="$1"
    local input="$2"
    if [[ -n "${ZSH_VERSION:-}" ]]; then
        # zsh: use parameter expansion to split on |
        eval "$arr_name=(\"\${(@s:|:)input}\")"
    else
        # bash: use read -ra
        eval "IFS='|' read -ra $arr_name <<< \"\$input\""
    fi
}

# Parse issue breakdown from goal-tracker.md
# Returns: blocking_issues|queued_issues|open_issues
humanize_parse_goal_tracker_issue_counts() {
    local tracker_file="$1"
    if [[ ! -f "$tracker_file" ]]; then
        echo "0|0|0"
        return
    fi

    _count_table_data_rows() {
        local row_count
        row_count=$(sed -n "/$1/,/$2/p" "$tracker_file" | grep -cE '^\|' || true)
        row_count=${row_count:-0}
        echo $((row_count > 2 ? row_count - 2 : 0))
    }

    local blocking_issues
    local queued_issues
    local open_issues

    blocking_issues=$(_count_table_data_rows '### Blocking Side Issues' '^###')
    queued_issues=$(_count_table_data_rows '### Queued Side Issues' '^###')
    open_issues=$((blocking_issues + queued_issues))

    # Legacy schema only had Open Issues; treat them as blocking for safety.
    if [[ "$open_issues" -eq 0 ]]; then
        open_issues=$(_count_table_data_rows '### Open Issues' '^###')
        blocking_issues="$open_issues"
    fi

    echo "${blocking_issues}|${queued_issues}|${open_issues}"
}

# Parse goal-tracker.md and return summary values
# Returns: total_acs|completed_acs|active_tasks|completed_tasks|deferred_tasks|open_issues|goal_summary
humanize_parse_goal_tracker() {
    local tracker_file="$1"
    if [[ ! -f "$tracker_file" ]]; then
        echo "0|0|0|0|0|0|No goal tracker"
        return
    fi

    # Helper: count data rows in a markdown table section (total rows minus header and separator)
    # Usage: _count_table_data_rows "section_start_pattern" "section_end_pattern"
    _count_table_data_rows() {
        local row_count
        row_count=$(sed -n "/$1/,/$2/p" "$tracker_file" | grep -cE '^\|' || true)
        row_count=${row_count:-0}
        echo $((row_count > 2 ? row_count - 2 : 0))
    }

    # Count Acceptance Criteria (supports both table and list formats)
    # Extracts unique AC identifiers (AC-1, AC-2.5, etc.) from the section,
    # using the same methodology as completed_acs to keep counts consistent
    local total_acs
    total_acs=$(sed -n '/### Acceptance Criteria/,/^---$/p' "$tracker_file" \
        | grep -aoE 'AC-?[0-9]+(\.[0-9]+)?' | sort -u | wc -l | tr -d ' ')
    total_acs=${total_acs:-0}

    # Count Active Tasks (tasks that are NOT completed AND NOT deferred)
    # This counts tasks with status: pending, partial, in_progress, todo, etc.
    local active_tasks
    local total_active_section_rows
    local completed_in_active
    local deferred_in_active

    # Count total table rows in Active Tasks section (includes header and separator)
    total_active_section_rows=$(sed -n '/#### Active Tasks/,/^###/p' "$tracker_file" \
        | grep -cE '^\|' || true)
    total_active_section_rows=${total_active_section_rows:-0}
    # Subtract header row and separator row (2 rows)
    local total_active_data_rows=$((total_active_section_rows > 2 ? total_active_section_rows - 2 : 0))

    # Count completed tasks in Active Tasks section (status column contains "completed")
    completed_in_active=$(sed -n '/#### Active Tasks/,/^###/p' "$tracker_file" \
        | sed 's/\*\*//g' \
        | grep -ciE '^\|[^|]+\|[^|]+\|[[:space:]]*completed[[:space:]]*\|' || true)
    completed_in_active=${completed_in_active:-0}

    # Count deferred tasks in Active Tasks section (status column contains "deferred")
    deferred_in_active=$(sed -n '/#### Active Tasks/,/^###/p' "$tracker_file" \
        | sed 's/\*\*//g' \
        | grep -ciE '^\|[^|]+\|[^|]+\|[[:space:]]*deferred[[:space:]]*\|' || true)
    deferred_in_active=${deferred_in_active:-0}

    # Active = total data rows - completed - deferred
    active_tasks=$((total_active_data_rows - completed_in_active - deferred_in_active))
    [[ "$active_tasks" -lt 0 ]] && active_tasks=0

    # Count Completed tasks
    local completed_tasks
    completed_tasks=$(_count_table_data_rows '### Completed and Verified' '^###')

    # Count verified ACs (unique AC entries in Completed section)
    # Extracts all AC identifiers (AC-1, AC1, AC-2.5, etc.) from anywhere in the section,
    # not just line-start, to handle rows with multiple comma-separated ACs (e.g. swarm mode)
    local completed_acs
    completed_acs=$(sed -n '/### Completed and Verified/,/^###/p' "$tracker_file" \
        | grep -aoE 'AC-?[0-9]+(\.[0-9]+)?' | sort -u | wc -l | tr -d ' ')
    completed_acs=${completed_acs:-0}

    # Count Deferred tasks
    local deferred_tasks
    deferred_tasks=$(_count_table_data_rows '### Explicitly Deferred' '^###')

    # Count Open Issues (new schema prefers Blocking/Queued Side Issues; old schema used Open Issues)
    local -a issue_parts
    humanize_split_to_array issue_parts "$(humanize_parse_goal_tracker_issue_counts "$tracker_file")"
    local open_issues="${issue_parts[2]}"

    # Extract Ultimate Goal summary (first content line after heading)
    local goal_summary
    goal_summary=$(sed -n '/### Ultimate Goal/,/^###/p' "$tracker_file" \
        | grep -v '^###' | grep -v '^$' | grep -v '^\[To be' \
        | head -1 | sed 's/^[[:space:]]*//')
    goal_summary="${goal_summary:-No goal defined}"

    echo "${total_acs}|${completed_acs}|${active_tasks}|${completed_tasks}|${deferred_tasks}|${open_issues}|${goal_summary}"
}

# Detect special git repository states
# Returns: state_name (one of: normal, detached, rebase, merge, shallow, permission_error)
humanize_detect_git_state() {
    local git_dir

    # Check if we're in a git repo and can access it
    git_dir=$(git rev-parse --git-dir 2>/dev/null) || {
        # Check if it's a permission issue vs not a repo
        if [[ -d ".git" ]] && ! [[ -r ".git" ]]; then
            echo "permission_error"
        else
            echo "not_a_repo"
        fi
        return
    }

    # Check for permission errors on git dir
    if ! [[ -r "$git_dir" ]]; then
        echo "permission_error"
        return
    fi

    # Check for rebase in progress
    if [[ -d "$git_dir/rebase-merge" ]] || [[ -d "$git_dir/rebase-apply" ]]; then
        echo "rebase"
        return
    fi

    # Check for merge in progress
    if [[ -f "$git_dir/MERGE_HEAD" ]]; then
        echo "merge"
        return
    fi

    # Check for shallow clone
    if [[ -f "$git_dir/shallow" ]]; then
        echo "shallow"
        return
    fi

    # Check for detached HEAD
    local head_ref
    head_ref=$(git symbolic-ref HEAD 2>/dev/null) || {
        echo "detached"
        return
    }

    echo "normal"
}

# Parse git status and return summary values
# Returns: modified|added|deleted|untracked|insertions|deletions
humanize_parse_git_status() {
    # Check if we're in a git repo
    if ! git rev-parse --git-dir &>/dev/null 2>&1; then
        echo "0|0|0|0|0|0|not a git repo"
        return
    fi

    # Get porcelain status (fast, machine-readable)
    local git_status_output=$(git status --porcelain 2>/dev/null)

    # Count file states from status output
    local modified=0 added=0 deleted=0 untracked=0

    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        local xy="${line:0:2}"
        case "$xy" in
            "??") ((untracked++)) ;;
            "A "* | " A"* | "AM"*) ((added++)) ;;
            "D "* | " D"*) ((deleted++)) ;;
            "M "* | " M"* | "MM"*) ((modified++)) ;;
            "R "* | " R"*) ((modified++)) ;;  # Renamed counts as modified
            *)
                # Handle other cases (staged + unstaged combinations)
                [[ "${xy:0:1}" == "M" || "${xy:1:1}" == "M" ]] && ((modified++))
                [[ "${xy:0:1}" == "A" ]] && ((added++))
                [[ "${xy:0:1}" == "D" || "${xy:1:1}" == "D" ]] && ((deleted++))
                ;;
        esac
    done <<< "$git_status_output"

    # Get line changes (insertions/deletions) - diff of staged + unstaged
    local diffstat=$(git diff --shortstat HEAD 2>/dev/null || git diff --shortstat 2>/dev/null)
    local insertions=0 deletions=0

    if [[ -n "$diffstat" ]]; then
        # Parse: " 3 files changed, 45 insertions(+), 12 deletions(-)"
        insertions=$(echo "$diffstat" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)
        deletions=$(echo "$diffstat" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo 0)
    fi
    insertions=${insertions:-0}
    deletions=${deletions:-0}

    echo "${modified}|${added}|${deleted}|${untracked}|${insertions}|${deletions}"
}

# ========================================
# Monitor function
# ========================================

# Monitor the latest Codex run log from .humanize/rlcr
# Automatically switches to newer logs when they appear
# Features a fixed status bar at the top showing session info
_humanize_monitor_codex() {
    # Enable 0-indexed arrays in zsh for bash compatibility
    # This affects all _split_to_array calls within this function
    [[ -n "${ZSH_VERSION:-}" ]] && setopt localoptions ksharrays

    local loop_dir=".humanize/rlcr"
    local current_file=""
    local current_session_dir=""
    local check_interval=2  # seconds between checking for new files
    local status_bar_height=11  # number of lines for status bar (includes loop status line)

    # Check if .humanize/rlcr exists
    if [[ ! -d "$loop_dir" ]]; then
        echo "Error: $loop_dir directory not found in current directory"
        echo "Are you in a project with an active humanize loop?"
        return 1
    fi

    # Use shared monitor helper for finding latest session
    _find_latest_session() {
        monitor_find_latest_session "$loop_dir"
    }

    # Function to find the latest codex log file for a specific session
    # Log files are now in $HOME/.cache/humanize/<sanitized-project-path>/<timestamp>/ to avoid context pollution
    # Respects XDG_CACHE_HOME for testability in restricted environments
    # Searches for both implementation phase logs (codex-run.log) and review phase logs (codex-review.log)
    # Usage: _find_latest_codex_log [session_dir]
    #   If session_dir is provided, only search within that session's cache directory
    #   If not provided, returns empty (we now require explicit session)
    _find_latest_codex_log() {
        local target_session_dir="$1"
        local latest=""
        local latest_round=-1
        local cache_base="${XDG_CACHE_HOME:-$HOME/.cache}/humanize"

        # Require explicit session directory to avoid showing logs from wrong session
        if [[ -z "$target_session_dir" || ! -d "$target_session_dir" ]]; then
            echo ""
            return
        fi

        # Get current project's absolute path and sanitize it
        # This matches the sanitization in loop-codex-stop-hook.sh
        local project_root="$(pwd)"
        local sanitized_project=$(echo "$project_root" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g')
        local project_cache_dir="$cache_base/$sanitized_project"

        local session_name=$(basename "$target_session_dir")

        # Helper to extract round number from log filename
        # Handles both codex-run.log and codex-review.log patterns
        _extract_round_num() {
            local basename="$1"
            local round="${basename#round-}"
            # Remove either -codex-run.log or -codex-review.log suffix
            round="${round%%-codex-run.log}"
            round="${round%%-codex-review.log}"
            echo "$round"
        }

        # Helper to detect log file type
        _is_review_log() {
            [[ "$1" == *-codex-review.log ]]
        }

        # Look for log files in the project-specific cache directory for this session
        local cache_dir="$project_cache_dir/$session_name"
        if [[ ! -d "$cache_dir" ]]; then
            echo ""
            return
        fi

        # Track max round numbers for each log type (for consistency check)
        local max_run_round=-1
        local min_review_round=-1

        # Search for both implementation phase (codex-run) and review phase (codex-review) logs
        # Use find with -o (OR) to match both patterns
        while IFS= read -r log_file; do
            [[ -z "$log_file" ]] && continue
            [[ ! -f "$log_file" ]] && continue

            local log_basename=$(basename "$log_file")
            local round_num=$(_extract_round_num "$log_basename")

            # Track round numbers by type for consistency check
            if _is_review_log "$log_basename"; then
                if [[ "$min_review_round" -eq -1 ]] || [[ "$round_num" -lt "$min_review_round" ]]; then
                    min_review_round="$round_num"
                fi
            else
                if [[ "$round_num" -gt "$max_run_round" ]]; then
                    max_run_round="$round_num"
                fi
            fi

            if [[ -z "$latest" ]] || [[ "$round_num" -gt "$latest_round" ]]; then
                latest="$log_file"
                latest_round="$round_num"
            fi
        done < <(find "$cache_dir" -maxdepth 1 \( -name 'round-*-codex-run.log' -o -name 'round-*-codex-review.log' \) -type f 2>/dev/null)

        # Defensive check: codex-run round must be strictly less than codex-review round
        # If review phase exists, all review rounds must be > all run rounds
        if [[ "$max_run_round" -ge 0 ]] && [[ "$min_review_round" -ge 0 ]]; then
            if [[ "$max_run_round" -ge "$min_review_round" ]]; then
                echo "ERROR: Inconsistent log state in session $session_name: codex-run round ($max_run_round) >= codex-review round ($min_review_round)" >&2
                echo ""
                return 1
            fi
        fi

        echo "$latest"
    }

    # Use shared monitor helper for finding state file
    _find_state_file() {
        monitor_find_state_file "$1"
    }

    # Parse state.md and return values
    _parse_state_md() {
        local state_file="$1"
        if [[ ! -f "$state_file" ]]; then
            echo "N/A|N/A|N/A|N/A|N/A|N/A|N/A|false|false||"
            return
        fi

        local current_round=$(grep -E "^current_round:" "$state_file" 2>/dev/null | sed 's/current_round: *//')
        local max_iterations=$(grep -E "^max_iterations:" "$state_file" 2>/dev/null | sed 's/max_iterations: *//')
        local full_review_round=$(grep -E "^full_review_round:" "$state_file" 2>/dev/null | sed 's/full_review_round: *//')
        local codex_model=$(grep -E "^codex_model:" "$state_file" 2>/dev/null | sed 's/codex_model: *//')
        local codex_effort=$(grep -E "^codex_effort:" "$state_file" 2>/dev/null | sed 's/codex_effort: *//')
        local started_at=$(grep -E "^started_at:" "$state_file" 2>/dev/null | sed 's/started_at: *//')
        local plan_file=$(grep -E "^plan_file:" "$state_file" 2>/dev/null | sed 's/plan_file: *//')
        local ask_codex_question=$(grep -E "^ask_codex_question:" "$state_file" 2>/dev/null | sed 's/ask_codex_question: *//' | tr -d ' ')
        local review_started=$(grep -E "^review_started:" "$state_file" 2>/dev/null | sed 's/review_started: *//' | tr -d ' ')
        local agent_teams=$(grep -E "^agent_teams:" "$state_file" 2>/dev/null | sed 's/agent_teams: *//' | tr -d ' ')
        local push_every_round=$(grep -E "^push_every_round:" "$state_file" 2>/dev/null | sed 's/push_every_round: *//' | tr -d ' ')
        local mainline_stall_count=$(grep -E "^mainline_stall_count:" "$state_file" 2>/dev/null | sed 's/mainline_stall_count: *//' | tr -d ' ')
        local last_mainline_verdict=$(grep -E "^last_mainline_verdict:" "$state_file" 2>/dev/null | sed 's/last_mainline_verdict: *//' | tr -d ' ')
        local drift_status=$(grep -E "^drift_status:" "$state_file" 2>/dev/null | sed 's/drift_status: *//' | tr -d ' ')

        echo "${current_round:-N/A}|${max_iterations:-N/A}|${full_review_round:-N/A}|${codex_model:-N/A}|${codex_effort:-N/A}|${started_at:-N/A}|${plan_file:-N/A}|${ask_codex_question:-false}|${review_started:-false}|${agent_teams:-}|${push_every_round:-}|${mainline_stall_count:-0}|${last_mainline_verdict:-unknown}|${drift_status:-normal}"
    }

    # Internal wrappers that call top-level functions
    # These maintain backward compatibility within _humanize_monitor_codex
    _parse_goal_tracker() { humanize_parse_goal_tracker "$@"; }
    _parse_git_status() { humanize_parse_git_status "$@"; }
    _split_to_array() { humanize_split_to_array "$@"; }

    # Draw the status bar at the top
    _draw_status_bar() {
        # Note: ksharrays is set at _humanize_monitor_codex() level for zsh compatibility

        local session_dir="$1"
        local log_file="$2"
        local loop_status="$3"  # "active", "completed", "failed", etc.
        local goal_tracker_file="$session_dir/goal-tracker.md"
        local term_width=$(tput cols)

        # Find and parse state file (state.md or *-state.md)
        local -a state_file_parts
        _split_to_array state_file_parts "$(_find_state_file "$session_dir")"
        local state_file="${state_file_parts[0]}"
        # Use passed loop_status if provided, otherwise use detected status
        [[ -z "$loop_status" ]] && loop_status="${state_file_parts[1]}"

        # Parse state file
        local -a state_parts
        _split_to_array state_parts "$(_parse_state_md "$state_file")"
        local current_round="${state_parts[0]}"
        local max_iterations="${state_parts[1]}"
        local full_review_round="${state_parts[2]}"
        local codex_model="${state_parts[3]}"
        local codex_effort="${state_parts[4]}"
        local started_at="${state_parts[5]}"
        local plan_file="${state_parts[6]}"
        local ask_codex_question="${state_parts[7]:-false}"
        local review_started="${state_parts[8]:-false}"
        local agent_teams="${state_parts[9]:-}"
        local push_every_round="${state_parts[10]:-}"
        local mainline_stall_count="${state_parts[11]:-0}"
        local last_mainline_verdict="${state_parts[12]:-unknown}"
        local drift_status="${state_parts[13]:-normal}"

        # Parse goal-tracker.md
        local -a goal_parts
        _split_to_array goal_parts "$(_parse_goal_tracker "$goal_tracker_file")"
        local total_acs="${goal_parts[0]}"
        local completed_acs="${goal_parts[1]}"
        local active_tasks="${goal_parts[2]}"
        local completed_tasks="${goal_parts[3]}"
        local deferred_tasks="${goal_parts[4]}"
        local open_issues="${goal_parts[5]}"
        local goal_summary="${goal_parts[6]}"
        local -a issue_parts
        _split_to_array issue_parts "$(humanize_parse_goal_tracker_issue_counts "$goal_tracker_file")"
        local blocking_issues="${issue_parts[0]}"
        local queued_issues="${issue_parts[1]}"

        # Parse git status
        local -a git_parts
        _split_to_array git_parts "$(_parse_git_status)"
        local git_modified="${git_parts[0]}"
        local git_added="${git_parts[1]}"
        local git_deleted="${git_parts[2]}"
        local git_untracked="${git_parts[3]}"
        local git_insertions="${git_parts[4]}"
        local git_deletions="${git_parts[5]}"

        # Format started_at for display (convert UTC to local time)
        local start_display="$started_at"
        if [[ "$started_at" != "N/A" ]]; then
            # Convert ISO UTC format to local time
            # Input: 2026-01-29T18:45:46Z
            # Output: 2026-01-29 10:45:46 (local time)
            local utc_time=$(echo "$started_at" | sed 's/T/ /; s/Z//')
            start_display=$(date -d "$utc_time UTC" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$started_at")
        fi

        # Truncate strings for display (label column is ~10 chars)
        local max_display_len=$((term_width - 12))
        local plan_display="$plan_file"
        local goal_display="$goal_summary"
        # Bash-compatible string slicing
        if [[ ${#plan_file} -gt $max_display_len ]]; then
            local suffix_len=$((max_display_len - 3))
            plan_display="...${plan_file: -$suffix_len}"
        fi
        if [[ ${#goal_summary} -gt $max_display_len ]]; then
            local prefix_len=$((max_display_len - 3))
            goal_display="${goal_summary:0:$prefix_len}..."
        fi

        # Save cursor position and move to top
        tput sc
        tput cup 0 0

        # ANSI color codes
        local green="\033[1;32m" yellow="\033[1;33m" cyan="\033[1;36m"
        local magenta="\033[1;35m" red="\033[1;31m" reset="\033[0m"
        local bg="\033[44m" bold="\033[1m" dim="\033[2m"
        local blue="\033[1;34m" orange="\033[38;5;208m"
        local clr_eol="\033[K"  # Clear to end of line (reduces flicker vs clearing entire area)

        # Move to top and draw directly (no pre-clearing to avoid flicker)
        tput cup 0 0
        printf "${bg}${bold}%-${term_width}s${reset}${clr_eol}\n" " Humanize Loop Monitor"
        printf "${cyan}Session Started:${reset} %s${clr_eol}\n" "$start_display"
        # Format full_review_round display (show in parentheses if available)
        local full_review_display=""
        if [[ "$full_review_round" != "N/A" && -n "$full_review_round" ]]; then
            full_review_display=" (${full_review_round})"
        fi
        # Build push_every_round segment if set
        local push_segment=""
        if [[ -n "$push_every_round" ]]; then
            local push_display="No"
            local push_color="${yellow}"
            if [[ "$push_every_round" == "true" ]]; then
                push_display="Yes"
                push_color="${green}"
            fi
            push_segment=" | Push Every Round: ${push_color}${push_display}${reset}"
        fi
        printf "${green}Round:${reset}    ${bold}%s${reset} / %s%s | ${yellow}Model:${reset} %s (%s)${push_segment}${clr_eol}\n" "$current_round" "$max_iterations" "$full_review_display" "$codex_model" "$codex_effort"

        # Loop status line with color based on status
        # Colors: Active=yellow, Complete=green, Finalize=cyan, Stop states=red, Others=orange
        local status_line=""
        case "$loop_status" in
            active)
                # Show phase with build->review format using colors
                # build phase: build=yellow, ->review=dim (no round numbers)
                # review phase: build(N)->review(M) with round numbers if available
                if [[ "$review_started" == "true" ]]; then
                    # Try to read build_finish_round from marker file for round display
                    local build_finish_round=""
                    local marker_file="$session_dir/.review-phase-started"
                    if [[ -f "$marker_file" ]]; then
                        build_finish_round=$(grep -oP '(?<=^build_finish_round=)\d+' "$marker_file" 2>/dev/null || true)
                    fi
                    if [[ -n "$build_finish_round" ]]; then
                        local review_rounds=$((current_round - build_finish_round))
                        status_line="${yellow}Active${reset}(${green}build(${build_finish_round})->${reset}${yellow}review(${review_rounds})${reset})"
                    else
                        status_line="${yellow}Active${reset}(${green}build->${reset}${yellow}review${reset})"
                    fi
                else
                    status_line="${yellow}Active${reset}(${yellow}build${reset}${dim}->review${reset})"
                fi
                ;;
            complete|completed)
                # Success state - green
                status_line="${green}Complete${reset}"
                ;;
            finalize)
                # Transitional state before completion - cyan
                status_line="${cyan}Finalize${reset}"
                ;;
            stop|cancel|cancelled|maxiter|unexpected|failed|error|timeout)
                # Stop/termination states - red
                local first_char=$(echo "${loop_status:0:1}" | tr '[:lower:]' '[:upper:]')
                local rest="${loop_status:1}"
                status_line="${red}${first_char}${rest}${reset}"
                ;;
            *)
                # Others (unknown, etc.) - orange
                local first_char=$(echo "${loop_status:0:1}" | tr '[:lower:]' '[:upper:]')
                local rest="${loop_status:1}"
                status_line="${orange}${first_char}${rest}${reset}"
                ;;
        esac
        # Display ask_codex_question setting (On/Off)
        local ask_q_display="Off"
        local ask_q_color="${dim}"
        if [[ "$ask_codex_question" == "true" ]]; then
            ask_q_display="On"
            ask_q_color="${green}"
        fi
        # Build team mode display if agent_teams is set
        local team_mode_segment=""
        if [[ -n "$agent_teams" ]]; then
            local team_display="Off"
            local team_color="${yellow}"
            if [[ "$agent_teams" == "true" ]]; then
                team_display="On"
                team_color="${green}"
            fi
            team_mode_segment=" | Team Mode: ${team_color}${team_display}${reset}"
        fi
        local drift_segment=""
        local drift_color="${dim}"
        if [[ "$drift_status" == "replan_required" ]]; then
            drift_color="${red}"
        elif [[ "${mainline_stall_count:-0}" -gt 0 ]]; then
            drift_color="${yellow}"
        fi
        if [[ -n "$drift_status" ]]; then
            drift_segment=" | Drift: ${drift_color}${drift_status}${reset} (${mainline_stall_count}, ${last_mainline_verdict})"
        fi
        printf "${magenta}Status:${reset}   ${status_line} | Codex Ask Question: ${ask_q_color}${ask_q_display}${reset}${team_mode_segment}${drift_segment}${clr_eol}\n"

        # Progress line (color based on completion status)
        local ac_color="${green}"
        [[ "$completed_acs" -lt "$total_acs" ]] && ac_color="${yellow}"
        local issue_total_color="${dim}"
        [[ "$queued_issues" -gt 0 ]] && issue_total_color="${yellow}"
        [[ "$blocking_issues" -gt 0 ]] && issue_total_color="${red}"

        # Use magenta for Progress and Git labels (status/data lines)
        printf "${magenta}Progress:${reset} ${ac_color}ACs: ${completed_acs}/${total_acs}${reset}  Tasks: ${active_tasks} active, ${completed_tasks} done"
        [[ "$deferred_tasks" -gt 0 ]] && printf "  ${yellow}${deferred_tasks} deferred${reset}"
        if [[ "$open_issues" -gt 0 ]]; then
            printf "  ${issue_total_color}Issues: ${open_issues}${reset}"
            [[ "$blocking_issues" -gt 0 ]] && printf " (${red}%s blocking${reset}" "$blocking_issues"
            [[ "$queued_issues" -gt 0 ]] && printf "%s${yellow}%s queued${reset}" \
                "$([[ "$blocking_issues" -gt 0 ]] && echo ", " || echo "(")" "$queued_issues"
            printf ")"
        fi
        printf "${clr_eol}\n"

        # Git status line (same color as Progress)
        local git_total=$((git_modified + git_added + git_deleted))
        printf "${magenta}Git:${reset}      "
        if [[ "$git_total" -eq 0 && "$git_untracked" -eq 0 ]]; then
            printf "${dim}clean${reset}"
        else
            [[ "$git_modified" -gt 0 ]] && printf "${yellow}~${git_modified}${reset} "
            [[ "$git_added" -gt 0 ]] && printf "${green}+${git_added}${reset} "
            [[ "$git_deleted" -gt 0 ]] && printf "${red}-${git_deleted}${reset} "
            [[ "$git_untracked" -gt 0 ]] && printf "${dim}?${git_untracked}${reset} "
            printf " ${green}+${git_insertions}${reset}/${red}-${git_deletions}${reset} lines"
        fi
        printf "${clr_eol}\n"

        # Use cyan for Goal, Plan, Log labels (context/reference lines)
        printf "${cyan}Goal:${reset}     %s${clr_eol}\n" "$goal_display"
        printf "${cyan}Plan:${reset}     %s${clr_eol}\n" "$plan_display"
        printf "${cyan}Log:${reset}      %s${clr_eol}\n" "$log_file"
        printf "%.s─" $(seq 1 $term_width)
        printf "${clr_eol}\n"

        # Restore cursor position
        tput rc
    }

    # Setup terminal for split view
    _setup_terminal() {
        # Clear screen
        clear
        # Set scroll region (leave top lines for status bar)
        printf "\033[${status_bar_height};%dr" $(tput lines)
        # Move cursor to scroll region
        tput cup $status_bar_height 0
    }

    # Check if terminal is too small for the monitor
    # Returns 0 if OK, 1 if too small
    _check_terminal_size() {
        local term_height=$(tput lines)
        local min_height=$((status_bar_height + 3))  # status bar + at least 3 lines for content
        if [[ "$term_height" -lt "$min_height" ]]; then
            return 1
        fi
        return 0
    }

    # Display terminal too small message
    _display_terminal_too_small() {
        local term_width=$(tput cols)
        local term_height=$(tput lines)
        local min_height=$((status_bar_height + 3))
        local message="This Humanize Monitor requires at least $min_height lines to work"
        local msg_len=${#message}
        local center_row=$((term_height / 2))
        local start_col=$(( (term_width - msg_len) / 2 ))
        [[ "$start_col" -lt 0 ]] && start_col=0

        # Reset scroll region and clear screen
        printf "\033[r"
        clear
        tput cup $center_row $start_col
        printf "%s" "$message"
    }

    # Update scroll region on terminal resize
    _update_scroll_region() {
        local new_lines=$(tput lines)
        # Update scroll region to new terminal height
        printf "\033[${status_bar_height};%dr" "$new_lines"
        # Clear the log area to remove any status bar remnants
        tput cup $status_bar_height 0
        tput ed  # Clear from cursor to end of screen
    }

    # Get the number of lines available for log display
    _get_log_area_height() {
        local term_height=$(tput lines)
        echo $((term_height - status_bar_height))
    }

    # Restore terminal to normal
    _restore_terminal() {
        # Reset scroll region to full screen
        printf "\033[r"
        # Move to bottom
        tput cup $(tput lines) 0
    }

    # Display centered message in the log area (for waiting states)
    _display_centered_message() {
        local message="$1"
        local term_width=$(tput cols)
        local term_height=$(tput lines)
        local content_height=$((term_height - status_bar_height))
        local center_row=$((status_bar_height + content_height / 2))
        local msg_len=${#message}
        local start_col=$(( (term_width - msg_len) / 2 ))
        [[ "$start_col" -lt 0 ]] && start_col=0

        tput cup $status_bar_height 0
        tput ed  # Clear log area
        tput cup $center_row $start_col
        printf "%s" "$message"
    }

    # Track PIDs for cleanup
    local tail_pid=""
    local monitor_running=true
    local cleanup_done=false

    # Cleanup function - called by trap
    # Must work cleanly in both bash and zsh
    _cleanup() {
        # Prevent multiple cleanup calls
        [[ "${cleanup_done:-false}" == "true" ]] && return
        cleanup_done=true
        monitor_running=false

        # Reset traps to prevent re-triggering
        # Use explicit signal numbers for better zsh compatibility
        trap - INT TERM WINCH 2>/dev/null || true

        # Kill background processes more robustly
        if [[ -n "$tail_pid" ]]; then
            # Check if process exists before killing
            if kill -0 "$tail_pid" 2>/dev/null; then
                kill "$tail_pid" 2>/dev/null || true
                # Use timeout-safe wait
                ( wait "$tail_pid" 2>/dev/null ) &
                wait $! 2>/dev/null || true
            fi
        fi

        _restore_terminal
        echo ""
        echo "Stopped monitoring."
    }

    # Graceful stop when loop directory is deleted
    # Per R1.2: calls _cleanup() to restore terminal state
    _graceful_stop() {
        local reason="$1"
        # Prevent multiple cleanup calls (checked again in _cleanup but check here too)
        [[ "${cleanup_done:-false}" == "true" ]] && return

        # Call _cleanup to do the actual cleanup work (per plan requirement)
        _cleanup

        # Print the specific graceful stop message after cleanup
        echo "Monitoring stopped: $reason"
        echo "The RLCR loop may have been cancelled or the directory was deleted."
    }

    # Track if resize happened (for main loop to detect)
    # IMPORTANT: SIGWINCH handler must only set flag, not call functions that output escape sequences
    # Otherwise it can race with _draw_status_bar and corrupt math expressions
    local resize_needed=false

    # Set up signal handlers (bash/zsh compatible)
    # Use function name without quotes for zsh compatibility
    # In zsh, traps in functions are local by default when using POSIX_TRAPS option
    if [[ -n "${ZSH_VERSION:-}" ]]; then
        # zsh: use TRAPINT and TRAPTERM for better handling
        TRAPINT() { _cleanup; return 130; }
        TRAPTERM() { _cleanup; return 143; }
        TRAPWINCH() { resize_needed=true; }
    else
        # bash: use standard trap
        trap '_cleanup' INT TERM
        trap 'resize_needed=true' WINCH
    fi

    # Find initial session and log file (only search within the current session)
    current_session_dir=$(_find_latest_session)
    current_file=$(_find_latest_codex_log "$current_session_dir")

    # Check if we have a valid session directory
    if [[ -z "$current_session_dir" ]]; then
        echo "No session directories found in $loop_dir"
        echo "Start an RLCR loop first with /humanize:start-rlcr-loop"
        return 1
    fi

    # Get loop status from state file
    local -a state_file_info
    _split_to_array state_file_info "$(_find_state_file "$current_session_dir")"
    local current_state_file="${state_file_info[0]}"
    local current_loop_status="${state_file_info[1]}"

    # Check initial terminal size
    if ! _check_terminal_size; then
        _display_terminal_too_small
        # Wait for resize to a larger size
        while ! _check_terminal_size; do
            sleep 0.5
            [[ "$resize_needed" == "true" ]] && resize_needed=false
        done
    fi

    # Setup terminal
    _setup_terminal

    # Use shared monitor helper for file size
    _get_file_size() {
        monitor_get_file_size "$1"
    }

    # Track last read position for incremental reading
    local last_size=0
    local file_size=0
    local last_no_log_status=""  # Track last rendered no-log status for refresh

    # Main monitoring loop
    while [[ "$monitor_running" == "true" ]]; do
        # Check if loop directory still exists (graceful exit if deleted)
        if [[ ! -d "$loop_dir" ]]; then
            _graceful_stop ".humanize/rlcr directory no longer exists"
            return 0
        fi

        # Update loop status
        _split_to_array state_file_info "$(_find_state_file "$current_session_dir")"
        current_state_file="${state_file_info[0]}"
        current_loop_status="${state_file_info[1]}"

        # Handle terminal resize at a safe point (before drawing)
        if [[ "$resize_needed" == "true" ]]; then
            resize_needed=false
            # Check if terminal is too small
            if ! _check_terminal_size; then
                _display_terminal_too_small
                # Wait for resize to a larger size
                while [[ "$monitor_running" == "true" ]] && ! _check_terminal_size; do
                    sleep 0.5
                    [[ "$resize_needed" == "true" ]] && resize_needed=false
                done
                [[ "$monitor_running" != "true" ]] && break
                # Terminal is now big enough, reinitialize
                _setup_terminal
            else
                _update_scroll_region
            fi
            # Re-display recent log content after resize (fill the log area)
            if [[ -n "$current_file" && -f "$current_file" ]]; then
                local log_lines=$(_get_log_area_height)
                tail -n "$log_lines" "$current_file" 2>/dev/null
            fi
        fi

        # Draw status bar (check flag before expensive operation)
        [[ "$monitor_running" != "true" ]] && break
        _draw_status_bar "$current_session_dir" "${current_file:-N/A}" "$current_loop_status"
        [[ "$monitor_running" != "true" ]] && break

        # Move cursor to scroll region
        tput cup $status_bar_height 0

        # Handle case when no log file exists for current session
        if [[ -z "$current_file" ]]; then
            # Track terminal dimensions to detect resize (fallback for SIGWINCH)
            local centered_last_cols=$(tput cols)
            local centered_last_rows=$(tput lines)

            # Render centered no-log message if status changed or not yet shown
            if [[ "$last_no_log_status" != "$current_loop_status" ]]; then
                if [[ "$current_loop_status" == "active" ]]; then
                    _display_centered_message "No Codex run or review started, please wait for the first run/review"
                else
                    _display_centered_message "No log file available for this session (status: $current_loop_status)"
                fi
                last_no_log_status="$current_loop_status"
            fi

            # Poll for new log files (only within current session)
            while [[ "$monitor_running" == "true" ]]; do
                sleep 0.5
                [[ "$monitor_running" != "true" ]] && break

                # Check if loop directory still exists (graceful exit if deleted)
                if [[ ! -d "$loop_dir" ]]; then
                    _graceful_stop ".humanize/rlcr directory no longer exists"
                    return 0
                fi

                # Detect terminal resize via both SIGWINCH flag and actual dimension change
                local redraw_centered_msg=false
                local cur_cols=$(tput cols)
                local cur_rows=$(tput lines)
                if [[ "$resize_needed" == "true" ]] || \
                   [[ "$cur_cols" != "$centered_last_cols" ]] || \
                   [[ "$cur_rows" != "$centered_last_rows" ]]; then
                    resize_needed=false
                    redraw_centered_msg=true
                    centered_last_cols="$cur_cols"
                    centered_last_rows="$cur_rows"
                    # Check if terminal is too small
                    if ! _check_terminal_size; then
                        _display_terminal_too_small
                        # Wait for resize to a larger size
                        while [[ "$monitor_running" == "true" ]] && ! _check_terminal_size; do
                            sleep 0.5
                            [[ "$resize_needed" == "true" ]] && resize_needed=false
                        done
                        [[ "$monitor_running" != "true" ]] && break
                        # Terminal is now big enough, reinitialize
                        _setup_terminal
                        centered_last_cols=$(tput cols)
                        centered_last_rows=$(tput lines)
                    else
                        _update_scroll_region
                    fi
                fi

                # Update loop status and redraw status bar
                _split_to_array state_file_info "$(_find_state_file "$current_session_dir")"
                current_loop_status="${state_file_info[1]}"
                _draw_status_bar "$current_session_dir" "N/A" "$current_loop_status"
                [[ "$monitor_running" != "true" ]] && break

                # Re-render no-log message if loop status changed or terminal resized
                if [[ "$last_no_log_status" != "$current_loop_status" ]] || [[ "$redraw_centered_msg" == "true" ]]; then
                    if [[ "$current_loop_status" == "active" ]]; then
                        _display_centered_message "No Codex run or review started, please wait for the first run/review"
                    else
                        _display_centered_message "No log file available for this session (status: $current_loop_status)"
                    fi
                    last_no_log_status="$current_loop_status"
                fi

                # Check for new log files within current session only
                local latest_session=$(_find_latest_session)
                [[ "$monitor_running" != "true" ]] && break

                # Handle session directory deletion
                if [[ ! -d "$current_session_dir" ]]; then
                    if [[ -n "$latest_session" ]]; then
                        # Current session deleted but another exists - switch to it
                        current_session_dir="$latest_session"
                        current_file=$(_find_latest_codex_log "$current_session_dir")
                        last_no_log_status=""  # Reset to re-render status for new session
                        tput cup $status_bar_height 0
                        tput ed
                        printf "\n==> Session directory deleted, switching to: %s\n" "$(basename "$latest_session")"
                        if [[ -n "$current_file" ]]; then
                            printf "==> Log: %s\n\n" "$current_file"
                            last_size=0
                            break
                        else
                            _display_centered_message "No Codex run or review started, please wait for the first run/review"
                        fi
                        continue
                    else
                        # No sessions available - wait for new ones
                        last_no_log_status=""  # Reset to re-render status
                        _display_centered_message "Session directory deleted, waiting for new sessions..."
                        current_session_dir=""
                        current_file=""
                        continue
                    fi
                fi

                # Update session dir immediately when a newer one exists (even without log)
                if [[ -n "$latest_session" && "$latest_session" != "$current_session_dir" ]]; then
                    current_session_dir="$latest_session"
                    last_no_log_status=""  # Reset to re-render status for new session
                fi

                # Check for log files within the current session only
                local latest=$(_find_latest_codex_log "$current_session_dir")
                [[ "$monitor_running" != "true" ]] && break

                if [[ -n "$latest" ]]; then
                    current_file="$latest"
                    last_no_log_status=""  # Reset for next no-log scenario
                    tput cup $status_bar_height 0
                    tput ed
                    printf "\n==> Log file found: %s\n\n" "$current_file"
                    last_size=0
                    break
                fi
            done
            continue
        fi

        # Get initial file size
        last_size=$(_get_file_size "$current_file")

        # Show existing content (fill the log area)
        [[ "$monitor_running" != "true" ]] && break
        local log_lines=$(_get_log_area_height)
        tail -n "$log_lines" "$current_file" 2>/dev/null

        # Track terminal dimensions to detect resize (fallback for SIGWINCH)
        local follow_last_cols=$(tput cols)
        local follow_last_rows=$(tput lines)

        # Incremental monitoring loop
        while [[ "$monitor_running" == "true" ]]; do
            sleep 0.5  # Check more frequently for smoother output
            [[ "$monitor_running" != "true" ]] && break

            # Check if loop directory still exists (graceful exit if deleted)
            if [[ ! -d "$loop_dir" ]]; then
                _graceful_stop ".humanize/rlcr directory no longer exists"
                return 0
            fi

            # Detect terminal resize via both SIGWINCH flag and actual dimension change
            local cur_cols=$(tput cols)
            local cur_rows=$(tput lines)
            if [[ "$resize_needed" == "true" ]] || \
               [[ "$cur_cols" != "$follow_last_cols" ]] || \
               [[ "$cur_rows" != "$follow_last_rows" ]]; then
                resize_needed=false
                follow_last_cols="$cur_cols"
                follow_last_rows="$cur_rows"
                # Check if terminal is too small
                if ! _check_terminal_size; then
                    _display_terminal_too_small
                    # Wait for resize to a larger size
                    while [[ "$monitor_running" == "true" ]] && ! _check_terminal_size; do
                        sleep 0.5
                        [[ "$resize_needed" == "true" ]] && resize_needed=false
                    done
                    [[ "$monitor_running" != "true" ]] && break
                    # Terminal is now big enough, reinitialize
                    _setup_terminal
                    follow_last_cols=$(tput cols)
                    follow_last_rows=$(tput lines)
                else
                    _update_scroll_region
                fi
                # Re-display recent log content after resize (fill the log area)
                if [[ -n "$current_file" && -f "$current_file" ]]; then
                    local log_lines=$(_get_log_area_height)
                    tail -n "$log_lines" "$current_file" 2>/dev/null
                fi
            fi

            # Update loop status
            _split_to_array state_file_info "$(_find_state_file "$current_session_dir")"
            current_loop_status="${state_file_info[1]}"

            # Update status bar (check flag before expensive operation)
            [[ "$monitor_running" != "true" ]] && break
            _draw_status_bar "$current_session_dir" "$current_file" "$current_loop_status"
            [[ "$monitor_running" != "true" ]] && break

            # Check for new content in current file
            file_size=$(_get_file_size "$current_file")
            if [[ "$file_size" -gt "$last_size" ]]; then
                # Read and display new content
                [[ "$monitor_running" != "true" ]] && break
                tail -c +$((last_size + 1)) "$current_file" 2>/dev/null
                last_size="$file_size"
            elif [[ "$last_size" -gt 0 ]] && [[ "$file_size" -lt "$last_size" ]]; then
                # File truncated or rotated (R1.3: detect size becomes 0 unexpectedly)
                # Only trigger when file previously had content (last_size > 0)
                # This prevents treating new empty files as truncated
                tput cup $status_bar_height 0
                tput ed
                printf "\n==> Log file truncated/rotated, searching for new log...\n"
                current_file=""
                last_size=0
                last_no_log_status=""
                break
            fi
            [[ "$monitor_running" != "true" ]] && break

            # Check for newer session directories first
            local latest_session=$(_find_latest_session)
            [[ "$monitor_running" != "true" ]] && break

            # Handle current session directory or log file deletion
            if [[ ! -d "$current_session_dir" ]] || [[ ! -f "$current_file" ]]; then
                # Capture deletion state BEFORE reassigning variables
                local session_was_deleted=false
                [[ ! -d "$current_session_dir" ]] && session_was_deleted=true

                if [[ -n "$latest_session" ]]; then
                    # Session or log deleted but another session exists - switch to it
                    current_session_dir="$latest_session"
                    current_file=$(_find_latest_codex_log "$current_session_dir")
                    tput cup $status_bar_height 0
                    tput ed
                    if [[ "$session_was_deleted" == "true" ]]; then
                        printf "\n==> Session directory deleted, switching to: %s\n" "$(basename "$latest_session")"
                    else
                        printf "\n==> Log file deleted, switching to: %s\n" "$(basename "$latest_session")"
                    fi
                    if [[ -n "$current_file" ]]; then
                        printf "==> Log: %s\n\n" "$current_file"
                    else
                        _display_centered_message "No Codex run or review started, please wait for the first run/review"
                        last_no_log_status=""  # Reset to ensure no-log branch re-renders
                    fi
                    last_size=0
                    break
                else
                    # No sessions available - wait for new ones (outer loop will handle)
                    current_session_dir=""
                    current_file=""
                    last_no_log_status=""  # Reset to re-render status
                    _display_centered_message "Session/log deleted, waiting for new sessions..."
                    break
                fi
            fi

            # Check if a newer session exists (even without log file)
            if [[ -n "$latest_session" && "$latest_session" != "$current_session_dir" ]]; then
                # New session found - switch to it
                current_session_dir="$latest_session"
                local new_session_log=$(_find_latest_codex_log "$current_session_dir")

                # Clear scroll region and notify
                tput cup $status_bar_height 0
                tput ed
                printf "\n==> Switching to newer session: %s\n" "$(basename "$latest_session")"

                if [[ -n "$new_session_log" ]]; then
                    # New session has a log file
                    current_file="$new_session_log"
                    printf "==> Log: %s\n\n" "$current_file"
                else
                    # New session has no log file yet - let outer loop handle it
                    current_file=""
                    last_no_log_status=""  # Reset to ensure no-log branch re-renders
                    _display_centered_message "No Codex run or review started, please wait for the first run/review"
                fi

                # Reset for new session
                last_size=0
                break
            fi

            # Check for newer log files within current session
            local latest=$(_find_latest_codex_log "$current_session_dir")
            [[ "$monitor_running" != "true" ]] && break

            if [[ "$latest" != "$current_file" && -n "$latest" ]]; then
                # Same session, but new log file (e.g., new round)
                current_file="$latest"

                # Clear scroll region and notify
                tput cup $status_bar_height 0
                tput ed
                printf "\n==> Switching to newer log: %s\n\n" "$current_file"

                # Reset for new file
                last_size=0
                break
            fi
        done
    done

    # Reset trap handlers (zsh and bash)
    if [[ -n "${ZSH_VERSION:-}" ]]; then
        # zsh: undefine the TRAP* functions
        unfunction TRAPINT TRAPTERM TRAPWINCH 2>/dev/null || true
    else
        trap - INT TERM WINCH
    fi
}


# Launch the web dashboard for one project. Foreground by default
# (matches the UX of the other `humanize monitor` subcommands);
# `--daemon` delegates to the existing tmux-backed launcher.
#
# Pass-through flags (forwarded to viz/server/app.py):
#   --project <path>      Project root for the dashboard (default: cwd)
#   --port <int>          Bound port (default: auto, 18000-18099)
#   --host <addr>         Bind address (default: 127.0.0.1; remote auth
#                         enforcement lands with T11 in a later round)
#   --auth-token <token>  Bearer token for remote-mode auth (parsed and
#                         forwarded; full enforcement lands with T11)
#   --daemon              Run as a background tmux service via viz-start.sh
_humanize_monitor_web() {
    local project_dir
    project_dir="$(pwd)"
    local host="127.0.0.1"
    local port=""
    local auth_token=""
    local daemon=false

    local trust_proxy=false
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --project) project_dir="$2"; shift 2 ;;
            --host)    host="$2"; shift 2 ;;
            --port)    port="$2"; shift 2 ;;
            --auth-token) auth_token="$2"; shift 2 ;;
            --trust-proxy) trust_proxy=true; shift ;;
            --daemon)  daemon=true; shift ;;
            -h|--help)
                echo "Usage: humanize monitor web [--project <path>] [--host <addr>] [--port <int>] [--auth-token <tok>] [--trust-proxy] [--daemon]"
                return 0
                ;;
            *)
                echo "Error: unknown flag for 'monitor web': $1" >&2
                return 1
                ;;
        esac
    done

    project_dir="$(cd "$project_dir" 2>/dev/null && pwd)" || {
        echo "Error: project directory not found: $project_dir" >&2
        return 1
    }
    if [[ ! -d "$project_dir/.humanize" ]]; then
        echo "Error: $project_dir/.humanize/ does not exist" >&2
        echo "  This command must run inside a project initialized by humanize." >&2
        return 1
    fi

    local viz_root="$HUMANIZE_SCRIPT_DIR/../viz"
    local app_entry="$viz_root/server/app.py"
    local static_dir="$viz_root/static"
    local venv_dir="$project_dir/.humanize/viz-venv"
    local requirements="$viz_root/server/requirements.txt"

    if [[ "$daemon" == "true" ]]; then
        # Daemon mode: reuse the tmux-backed launcher (now per-project
        # named per T9). Forward every flag so remote-bind + token
        # configuration reach the underlying app.py invocation.
        local viz_start="$viz_root/scripts/viz-start.sh"
        if [[ ! -x "$viz_start" ]]; then
            echo "Error: viz-start.sh not found at $viz_start" >&2
            return 1
        fi
        local -a daemon_args=(--project "$project_dir" --host "$host")
        [[ -n "$port" ]] && daemon_args+=(--port "$port")
        [[ -n "$auth_token" ]] && daemon_args+=(--auth-token "$auth_token")
        [[ "$trust_proxy" == "true" ]] && daemon_args+=(--trust-proxy)
        bash "$viz_start" "${daemon_args[@]}"
        return $?
    fi

    # Foreground mode (default per DEC-1).
    if [[ ! -d "$venv_dir" ]]; then
        echo "Creating Python virtual environment for the dashboard..."
        python3 -m venv "$venv_dir" || {
            echo "Error: failed to create venv at $venv_dir" >&2
            return 1
        }
        echo "Installing dependencies..."
        "$venv_dir/bin/pip" install --quiet -r "$requirements" || {
            echo "Error: failed to install requirements" >&2
            return 1
        }
        touch "$venv_dir/.requirements_installed"
    elif [[ "$requirements" -nt "$venv_dir/.requirements_installed" ]]; then
        echo "Updating dependencies..."
        if ! "$venv_dir/bin/pip" install --quiet -r "$requirements"; then
            # Leave .requirements_installed untouched so the next
            # launch re-detects the stale marker and retries the
            # upgrade rather than silently starting with missing
            # packages. Surface a non-zero exit so callers see it.
            echo "Error: pip install failed during dependency refresh" >&2
            return 1
        fi
        touch "$venv_dir/.requirements_installed"
    fi

    if [[ -z "$port" ]]; then
        # Probe the requested bind host so port selection matches what
        # app.run(host=BIND_HOST, port=$port) will actually try to bind.
        # Loopback aliases and wildcards listen on localhost too, so
        # localhost is a valid proxy for them; but a specific non-
        # loopback address does NOT listen on localhost, so probing
        # localhost misses EADDRINUSE conflicts on the external
        # interface and Flask would die on startup. Mirrors the
        # Round 14 fix in viz/scripts/viz-start.sh:find_port.
        local probe_host
        case "$host" in
            127.0.0.1|::1|localhost|0.0.0.0|::)
                probe_host="localhost"
                ;;
            *)
                probe_host="$host"
                ;;
        esac
        for candidate in $(seq 18000 18099); do
            if ! (echo >/dev/tcp/$probe_host/$candidate) 2>/dev/null; then
                port="$candidate"
                break
            fi
        done
        if [[ -z "$port" ]]; then
            echo "Error: no available port in range 18000-18099" >&2
            return 1
        fi
    fi

    if [[ "$host" != "127.0.0.1" && "$host" != "localhost" && -z "$auth_token" ]]; then
        echo "Warning: binding $host without --auth-token (full remote auth enforcement is T11)" >&2
    fi

    local visible_host="$host"
    [[ "$host" == "127.0.0.1" || "$host" == "::1" ]] && visible_host="localhost"
    local url="http://${visible_host}:${port}"
    echo "Starting humanize monitor web at $url (project: $project_dir)"
    echo "Press Ctrl+C to stop."

    local -a fg_args=(
        --host "$host"
        --port "$port"
        --project "$project_dir"
        --static "$static_dir"
    )
    [[ -n "$auth_token" ]] && fg_args+=(--auth-token "$auth_token")
    [[ "$trust_proxy" == "true" ]] && fg_args+=(--trust-proxy)

    # Do NOT exec: `humanize` is a function sourced into the user's
    # interactive shell (see scripts/humanize.sh usage in README).
    # `exec` would replace that shell process with Python, so
    # pressing Ctrl+C (or any server exit) would kill the whole
    # interactive session. Running the command as a child process
    # instead lets the function return normally on server exit and
    # keeps the shell prompt alive.
    "$venv_dir/bin/python" "$app_entry" "${fg_args[@]}"
}


# Main humanize function
humanize() {
    local cmd="$1"
    shift

    case "$cmd" in
        monitor)
            local target="$1"
            shift 2>/dev/null || true
            case "$target" in
                rlcr)
                    _humanize_monitor_codex "$@"
                    ;;
                skill)
                    _humanize_monitor_skill "$@"
                    ;;
                codex)
                    _humanize_monitor_skill --tool-filter codex "$@"
                    ;;
                gemini)
                    _humanize_monitor_skill --tool-filter gemini "$@"
                    ;;
                web)
                    _humanize_monitor_web "$@"
                    ;;
                *)
                    echo "Usage: humanize monitor <rlcr|skill|codex|gemini|web>"
                    echo ""
                    echo "Subcommands:"
                    echo "  rlcr    Monitor the latest RLCR loop log from .humanize/rlcr"
                    echo "  skill   Monitor all skill invocations (codex + gemini)"
                    echo "  codex   Monitor ask-codex skill invocations only"
                    echo "  gemini  Monitor ask-gemini skill invocations only"
                    echo "  web     Launch the browser dashboard for one project"
                    echo ""
                    echo "Features (terminal monitors):"
                    echo "  - Fixed status bar showing session info, round progress, model config"
                    echo "  - Goal tracker summary: Ultimate Goal, AC progress, task status"
                    echo "  - Real-time log output in scrollable area below"
                    echo "  - Automatically switches to newer logs when they appear"
                    return 1
                    ;;
            esac
            ;;
        *)
            echo "Usage: humanize <command> [args]"
            echo ""
            echo "Commands:"
            echo "  monitor rlcr    Monitor the latest RLCR loop log"
            echo "  monitor skill   Monitor all skill invocations (codex + gemini)"
            echo "  monitor codex   Monitor ask-codex skill invocations only"
            echo "  monitor gemini  Monitor ask-gemini skill invocations only"
            echo "  monitor web     Launch the browser dashboard for one project"
            return 1
            ;;
    esac
}

# Source skill monitor (provides _humanize_monitor_skill)
if [[ -f "$HUMANIZE_SCRIPT_DIR/lib/monitor-skill.sh" ]]; then
    source "$HUMANIZE_SCRIPT_DIR/lib/monitor-skill.sh"
fi
