#!/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/// 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 Project root for the dashboard (default: cwd) # --port Bound port (default: auto, 18000-18099) # --host Bind address (default: 127.0.0.1; remote auth # enforcement lands with T11 in a later round) # --auth-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 ] [--host ] [--port ] [--auth-token ] [--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 " 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 [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