#!/usr/bin/env bash # # monitor-common.sh - Shared utilities for humanize monitor functions # # This file contains common functions used by humanize monitor functions. # It should be sourced by humanize.sh rather than executed directly. # ======================================== # ANSI Color Constants # ======================================== # These are defined as functions to allow dynamic evaluation # (some terminals may not support all colors) monitor_color_green() { echo "\033[1;32m"; } monitor_color_yellow() { echo "\033[1;33m"; } monitor_color_cyan() { echo "\033[1;36m"; } monitor_color_magenta() { echo "\033[1;35m"; } monitor_color_red() { echo "\033[1;31m"; } monitor_color_reset() { echo "\033[0m"; } monitor_color_bg() { echo "\033[44m"; } monitor_color_bold() { echo "\033[1m"; } monitor_color_dim() { echo "\033[2m"; } monitor_color_blue() { echo "\033[1;34m"; } # ======================================== # File Utilities # ======================================== # Get file size (cross-platform: Linux uses -c%s, macOS uses -f%z) # Usage: monitor_get_file_size "/path/to/file" # Returns: file size in bytes, or 0 if file doesn't exist monitor_get_file_size() { local file="$1" stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo 0 } # Find latest directory by timestamp name pattern (YYYY-MM-DD_HH-MM-SS) # Usage: monitor_find_latest_session "/path/to/loop/dir" # Returns: path to latest session directory, or empty string if none found monitor_find_latest_session() { local loop_dir="$1" local latest_session="" if [[ ! -d "$loop_dir" ]]; then echo "" return fi # Use find instead of glob to avoid zsh "no matches found" errors while IFS= read -r session_dir; do [[ -z "$session_dir" ]] && continue [[ ! -d "$session_dir" ]] && continue local session_name=$(basename "$session_dir") if [[ "$session_name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}$ ]]; then if [[ -z "$latest_session" ]] || [[ "$session_name" > "$(basename "$latest_session")" ]]; then latest_session="$session_dir" fi fi done < <(find "$loop_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null) echo "$latest_session" } # ======================================== # Terminal Utilities # ======================================== # Setup terminal for split view with fixed header # Usage: monitor_setup_terminal monitor_setup_terminal() { local header_height="$1" # Clear screen clear # Set scroll region (leave top lines for status bar) printf "\033[${header_height};%dr" $(tput lines) # Move cursor to scroll region tput cup "$header_height" 0 } # Restore terminal to normal state # Usage: monitor_restore_terminal monitor_restore_terminal() { # Reset scroll region to full screen printf "\033[r" # Move to bottom tput cup $(tput lines) 0 } # ======================================== # Signal Handling # ======================================== # Setup signal handlers for clean Ctrl+C handling # This function should be called with the cleanup function name as argument # # Usage: monitor_setup_signal_handlers "cleanup_function_name" # # The cleanup function should: # 1. Set a cleanup_done flag to prevent multiple calls # 2. Set monitor_running=false to stop loops # 3. Kill any background processes # 4. Restore terminal state # # Example cleanup function: # _cleanup() { # [[ "$cleanup_done" == "true" ]] && return # cleanup_done=true # monitor_running=false # trap - INT TERM 2>/dev/null || true # [[ -n "$TAIL_PID" ]] && kill "$TAIL_PID" 2>/dev/null # monitor_restore_terminal # echo "Stopped." # } # # Note: This function is a documentation reference. The actual signal # setup should be done inline in each monitor function for proper scope # handling of local variables (cleanup_done, monitor_running, etc.) # ======================================== # Status Color Helper # ======================================== # Get color code for loop status # Usage: color=$(monitor_get_status_color "active") monitor_get_status_color() { local status="$1" case "$status" in active|methodology-analysis) echo "\033[1;32m" ;; # green completed) echo "\033[1;36m" ;; # cyan failed|error|timeout) echo "\033[1;31m" ;; # red cancelled) echo "\033[1;33m" ;; # yellow max-iterations) echo "\033[1;31m" ;; # red unknown) echo "\033[2m" ;; # dim *) echo "\033[1;33m" ;; # yellow (default for unknown states) esac } # ======================================== # State File Detection # ======================================== # Find state file in session directory # Returns: state_file_path|loop_status # - If state.md exists: returns "path/state.md|active" # - If -state.md exists: returns "path/|" # - If no state file found: returns "|unknown" # # Usage: monitor_find_state_file "/path/to/session" monitor_find_state_file() { local session_dir="$1" if [[ -z "$session_dir" || ! -d "$session_dir" ]]; then echo "|unknown" return fi # Priority 1: Active state files indicate running loop if [[ -f "$session_dir/methodology-analysis-state.md" ]]; then echo "$session_dir/methodology-analysis-state.md|methodology-analysis" return fi if [[ -f "$session_dir/state.md" ]]; then echo "$session_dir/state.md|active" return fi # Priority 2: Look for -state.md files # Common stop reasons: completed, failed, cancelled, timeout, error, approve, maxiter local state_file="" local stop_reason="" while IFS= read -r f; do [[ -z "$f" ]] && continue if [[ -f "$f" ]]; then state_file="$f" # Extract stop reason from filename (e.g., "completed-state.md" -> "completed") local basename=$(basename "$f") stop_reason="${basename%-state.md}" break fi done < <(find "$session_dir" -maxdepth 1 -name '*-state.md' -type f 2>/dev/null) if [[ -n "$state_file" ]]; then echo "$state_file|$stop_reason" else echo "|unknown" fi } # ======================================== # YAML Frontmatter Parsing # ======================================== # Extract a value from YAML frontmatter # Usage: monitor_get_yaml_value "key" "/path/to/file.md" # Returns: The value, or empty string if not found monitor_get_yaml_value() { local key="$1" local file="$2" [[ ! -f "$file" ]] && return # Extract frontmatter (between first and second ---) local frontmatter frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$file" 2>/dev/null) # Extract value for key echo "$frontmatter" | grep -E "^${key}:" | sed "s/${key}: *//" | tr -d '"' } # ======================================== # Progress Display Helpers # ======================================== # Format a timestamp for display # Converts ISO format (2026-01-18T10:00:00Z) to readable format # Usage: monitor_format_timestamp "2026-01-18T10:00:00Z" monitor_format_timestamp() { local timestamp="$1" if [[ "$timestamp" == "N/A" || -z "$timestamp" ]]; then echo "N/A" return fi # Convert ISO format to more readable format echo "$timestamp" | sed 's/T/ /; s/Z/ UTC/' } # Truncate a string for display, adding ellipsis # Usage: monitor_truncate_string "long string" # direction: "start" (keep end) or "end" (keep start, default) monitor_truncate_string() { local str="$1" local max_len="$2" local direction="${3:-end}" if [[ ${#str} -le $max_len ]]; then echo "$str" return fi if [[ "$direction" == "start" ]]; then # Keep end, truncate start local suffix_len=$((max_len - 3)) echo "...${str: -$suffix_len}" else # Keep start, truncate end local prefix_len=$((max_len - 3)) echo "${str:0:$prefix_len}..." fi } # ======================================== # Goal Tracker Parsing # ======================================== # Parse issue breakdown from goal-tracker.md # Returns: blocking_issues|queued_issues|open_issues # Usage: parse_goal_tracker_issue_counts "/path/to/goal-tracker.md" parse_goal_tracker_issue_counts() { local tracker_file="$1" if [[ ! -f "$tracker_file" ]]; then echo "0|0|0" return fi _count_table_rows() { local start_pattern="$1" local end_pattern="$2" local row_count row_count=$(sed -n "/${start_pattern}/,/${end_pattern}/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_rows '### Blocking Side Issues' '^###') queued_issues=$(_count_table_rows '### Queued Side Issues' '^###') open_issues=$((blocking_issues + queued_issues)) if [[ "$open_issues" -eq 0 ]]; then open_issues=$(_count_table_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 # Usage: parse_goal_tracker "/path/to/goal-tracker.md" 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) _count_table_rows() { local start_pattern="$1" local end_pattern="$2" local row_count row_count=$(sed -n "/${start_pattern}/,/${end_pattern}/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) # Stop at next section header (##) to avoid counting ACs from other sections local total_acs total_acs=$(sed -n '/### Acceptance Criteria/,/^##/p' "$tracker_file" \ | grep -cE '(^\|\s*\*{0,2}[A]?[C]-?[0-9]+|^-\s*\*{0,2}[A]?[C]-?[0-9]+)' || true) total_acs=${total_acs:-0} # Count Active Tasks local total_active_section_rows local completed_in_active local deferred_in_active total_active_section_rows=$(sed -n '/#### Active Tasks/,/^###/p' "$tracker_file" \ | grep -cE '^\|' || true) total_active_section_rows=${total_active_section_rows:-0} local total_active_data_rows=$((total_active_section_rows > 2 ? total_active_section_rows - 2 : 0)) 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} 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} local 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_rows '### Completed and Verified' '^###') # Count verified ACs (unique AC entries in Completed section) local completed_acs completed_acs=$(sed -n '/### Completed and Verified/,/^###/p' "$tracker_file" \ | grep -oE '^\|\s*[A]?[C]-?[0-9]+' | sort -u | wc -l | tr -d ' ') completed_acs=${completed_acs:-0} # Count Deferred tasks local deferred_tasks deferred_tasks=$(_count_table_rows '### Explicitly Deferred' '^###') # Count Open Issues (new schema prefers Blocking/Queued Side Issues; old schema used Open Issues) local issue_parts_raw local open_issues issue_parts_raw=$(parse_goal_tracker_issue_counts "$tracker_file") if [[ -n "${ZSH_VERSION:-}" ]]; then local -a issue_parts issue_parts=("${(@s:|:)issue_parts_raw}") open_issues="${issue_parts[3]}" else local -a issue_parts IFS='|' read -r -a issue_parts <<< "$issue_parts_raw" open_issues="${issue_parts[2]}" fi # Extract Ultimate Goal summary 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:]]*//' | cut -c1-60) goal_summary="${goal_summary:-No goal defined}" echo "${total_acs}|${completed_acs}|${active_tasks}|${completed_tasks}|${deferred_tasks}|${open_issues}|${goal_summary}" }