/**
 * @fileoverview Ralph Tracker - Detects Ralph Wiggum loops, todos, and completion phrases.
 *
 * Parses terminal output from Claude Code sessions to detect:
 * - Ralph loop state (active, completion phrase, iteration count)
 * - Todo items from the TodoWrite tool (with deduplication and expiry)
 * - Completion phrases signaling loop completion
 * - Circuit breaker state (CLOSED/HALF_OPEN/OPEN)
 *
 * DISABLED by default — auto-enables when Ralph-related patterns appear,
 * reducing overhead for non-autonomous sessions.
 *
 * Composed of four sub-modules:
 * - `RalphPlanTracker`: Plan task management, checkpoints, versioning
 * - `RalphFixPlanWatcher`: @fix_plan.md file watching and parsing
 * - `RalphStallDetector`: Iteration stall detection
 * - `RalphStatusParser`: RALPH_STATUS block parsing, circuit breaker
 *
 * Key exports:
 * - `RalphTracker` class — main tracker, extends EventEmitter
 * - `RalphTrackerEvents` interface — typed event map
 *
 * Key methods: `processData(data)` — feed terminal output, `getState()`,
 * `getTodos()`, `getCompletionHistory()`, `getPlanTasks()`, `reset()`
 *
 * @dependencies types (RalphTrackerState, RalphTodoItem, CircuitBreakerStatus),
 *   ralph-plan-tracker, ralph-fix-plan-watcher, ralph-stall-detector, ralph-status-parser,
 *   config/buffer-limits, config/map-limits
 * @consumedby session (owns one RalphTracker per session), web/server (SSE events)
 * @emits ralphStateChanged, todoUpdated, completionDetected, statusBlockParsed,
 *   circuitBreakerChanged, exitGateMet, planTaskUpdated, planCheckpoint
 *
 * @module ralph-tracker
 */
import { EventEmitter } from 'node:events';
import { createInitialRalphTrackerState, } from './types.js';
import { ANSI_ESCAPE_PATTERN_SIMPLE, fuzzyPhraseMatch, todoContentHash, stringSimilarity, Debouncer, CleanupManager, execPattern, } from './utils/index.js';
import { MAX_LINE_BUFFER_SIZE } from './config/buffer-limits.js';
import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
import { RalphPlanTracker } from './ralph-plan-tracker.js';
import { RalphFixPlanWatcher, generateFixPlanMarkdown, importFixPlanMarkdown } from './ralph-fix-plan-watcher.js';
import { RalphStallDetector } from './ralph-stall-detector.js';
import { RalphStatusParser } from './ralph-status-parser.js';
import { STALE_DATA_MAX_AGE_MS, INACTIVITY_TIMEOUT_MS } from './config/server-timing.js';
// ========== Configuration Constants ==========
// Note: MAX_TODOS_PER_SESSION and MAX_LINE_BUFFER_SIZE are imported from config modules
/**
 * Todo items older than this duration (in milliseconds) will be auto-expired.
 * Default: 1 hour
 */
const TODO_EXPIRY_MS = STALE_DATA_MAX_AGE_MS;
/**
 * Minimum interval between on-demand cleanup checks (in milliseconds).
 * Prevents running cleanup on every data chunk.
 * Default: 30 seconds
 */
const CLEANUP_THROTTLE_MS = 30 * 1000;
/**
 * Interval for periodic todo expiry cleanup (in milliseconds).
 * Actively purges expired todos even when no terminal data is flowing.
 * Default: 5 minutes
 */
const TODO_CLEANUP_INTERVAL_MS = INACTIVITY_TIMEOUT_MS;
/**
 * Similarity threshold for todo deduplication.
 * Todos with similarity >= this value are considered duplicates.
 * Range: 0.0 (no similarity) to 1.0 (identical)
 * Default: 0.85 (85% similar)
 */
const TODO_SIMILARITY_THRESHOLD = 0.85;
/**
 * Similarity threshold for short todo content (<30 chars).
 * Higher threshold reduces false positive deduplication of short strings.
 */
const SIMILARITY_THRESHOLD_SHORT = 0.95;
/**
 * Similarity threshold for medium-length todo content (30-60 chars).
 * Slightly relaxed compared to short strings.
 */
const SIMILARITY_THRESHOLD_MEDIUM = 0.9;
/**
 * Debounce interval for event emissions (milliseconds).
 * Prevents UI jitter from rapid consecutive updates.
 * Default: 50ms
 */
const EVENT_DEBOUNCE_MS = 50;
/**
 * Maximum number of completion phrase entries to track.
 * Prevents unbounded growth if many unique phrases are seen.
 */
const MAX_COMPLETION_PHRASE_ENTRIES = 50;
/**
 * Common/generic completion phrases that may cause false positives.
 * These phrases are likely to appear in Claude's natural output,
 * making them unreliable as completion signals.
 *
 * P1-002: Configurable false positive prevention
 */
const COMMON_COMPLETION_PHRASES = new Set([
    'DONE',
    'COMPLETE',
    'FINISHED',
    'OK',
    'YES',
    'TRUE',
    'SUCCESS',
    'READY',
    'COMPLETED',
    'PASSED',
    'END',
    'STOP',
    'EXIT',
]);
/**
 * Minimum recommended phrase length for completion detection.
 * Shorter phrases are more likely to cause false positives.
 */
const MIN_RECOMMENDED_PHRASE_LENGTH = 6;
// ========== Pre-compiled Regex Patterns ==========
// Pre-compiled for performance (avoid re-compilation on each call)
/**
 * Matches completion phrase tags: `<promise>PHRASE</promise>`
 * Used to detect when Claude signals task completion.
 * Capture group 1: The completion phrase text
 *
 * Supports any characters between tags including:
 * - Uppercase letters: COMPLETE, DONE
 * - Numbers: TASK_123
 * - Underscores: ALL_TASKS_DONE
 * - Hyphens: TESTS-PASS, TIME-COMPLETE
 *
 * Now also tolerates:
 * - Whitespace/newlines inside tags: <promise> COMPLETE </promise>
 * - Case variations in tag names: <Promise>, <PROMISE>
 */
const PROMISE_PATTERN = /<promise>\s*([^<]+?)\s*<\/promise>/i;
/**
 * Pattern for detecting partial/incomplete promise tags at end of buffer.
 * Used for cross-chunk promise detection when tags are split across PTY writes.
 * Captures:
 * - Group 1: Partial opening tag content after <promise> (may be incomplete)
 */
const PROMISE_PARTIAL_PATTERN = /<promise>\s*([^<]*)$/i;
// ---------- Todo Item Patterns ----------
// Claude Code outputs todos in multiple formats; we detect all of them
/**
 * Format 1: Markdown checkbox format
 * Matches: "- [ ] Task" or "- [x] Task" (also with * bullet)
 * Capture group 1: Checkbox state ('x', 'X', or ' ')
 * Capture group 2: Task content
 */
const TODO_CHECKBOX_PATTERN = /^[-*]\s*\[([xX ])\]\s+(.+)$/gm;
/**
 * Format 2: Todo with indicator icons
 * Matches: "Todo: ☐ Task", "Todo: ◐ Task", "Todo: ✓ Task"
 * Capture group 1: Status icon
 * Capture group 2: Task content
 */
const TODO_INDICATOR_PATTERN = /Todo:\s*(☐|◐|✓|⏳|✅|⌛|🔄)\s+(.+)/g;
/**
 * Format 3: Status in parentheses
 * Matches: "- Task (pending)", "- Task (in_progress)", "- Task (completed)"
 * Capture group 1: Task content
 * Capture group 2: Status string
 */
const TODO_STATUS_PATTERN = /[-*]\s*(.+?)\s+\((pending|in_progress|completed)\)/g;
/**
 * Format 4: Claude Code native TodoWrite output
 * Matches: "☐ Task", "☒ Task", "◐ Task", "✓ Task"
 * These appear with optional leading whitespace/brackets like "⎿  ☐ Task"
 * Capture group 1: Checkbox icon (☐=pending, ☒=completed, ◐=in_progress, ✓=completed)
 * Capture group 2: Task content (min 3 chars, excludes checkbox icons)
 */
const TODO_NATIVE_PATTERN = /^[\s⎿]*(☐|☒|◐|✓)\s+([^☐☒◐✓\n]{3,})/gm;
/**
 * Format 5: Claude Code checkmark-based TodoWrite output
 * Matches task creation: "✔ Task #1 created: Fix the bug"
 * Matches task summary: "✔ #1 Fix the bug"
 * Matches status update: "✔ Task #1 updated: status → completed"
 *
 * These are the primary output format of Claude Code's TodoWrite tool.
 */
const TODO_TASK_CREATED_PATTERN = /✔\s*Task\s*#(\d+)\s*created:\s*(.+)/g;
const TODO_TASK_SUMMARY_PATTERN = /✔\s*#(\d+)\s+(.+)/g;
const TODO_TASK_STATUS_PATTERN = /✔\s*Task\s*#(\d+)\s*updated:\s*status\s*→\s*(in progress|completed|pending)/g;
/**
 * Matches plain checkmark TodoWrite output without task numbers.
 * Real Claude Code TodoWrite output: "✔ Create hello.txt with Hello World"
 * This is the most common format in actual usage.
 */
const TODO_PLAIN_CHECKMARK_PATTERN = /✔\s+(.+)/g;
/**
 * Patterns to exclude from todo detection
 * Prevents false positives from tool invocations and Claude commentary
 */
const TODO_EXCLUDE_PATTERNS = [
    /^(?:Bash|Search|Read|Write|Glob|Grep|Edit|Task)\s*\(/i, // Tool invocations
    /^(?:I'll |Let me |Now I|First,|Task \d+:|Result:|Error:)/i, // Claude commentary
    /^\S+\([^)]+\)$/, // Generic function call pattern
];
// ---------- Loop Status Patterns ----------
// Note: <promise> tags are handled separately by PROMISE_PATTERN
/**
 * Matches generic loop start messages
 * Examples: "Loop started at", "Starting main loop", "Ralph loop started"
 */
const LOOP_START_PATTERN = /Loop started at|Starting.*loop|Ralph loop started/i;
/**
 * Matches elapsed time output
 * Example: "Elapsed: 2.5 hours"
 * Capture group 1: Hours as decimal number
 */
const ELAPSED_TIME_PATTERN = /Elapsed:\s*(\d+(?:\.\d+)?)\s*hours?/i;
/**
 * Matches cycle count indicators (legacy format)
 * Examples: "cycle #5", "respawn cycle #3"
 * Capture groups 1 or 2: Cycle number
 */
const CYCLE_PATTERN = /cycle\s*#?(\d+)|respawn cycle #(\d+)/i;
// ---------- Ralph Wiggum Plugin Patterns ----------
// Based on the official Ralph Wiggum plugin output format
/**
 * Matches iteration progress indicators
 * Examples: "Iteration 5/50", "[5/50]", "iteration #5", "iter. 3 of 10"
 * Capture groups: (1,2) for "Iteration X/Y" format, (3,4) for "[X/Y]" format
 */
const ITERATION_PATTERN = /(?:iteration|iter\.?)\s*#?(\d+)(?:\s*(?:\/|of)\s*(\d+))?|\[(\d+)\/(\d+)\]/i;
/**
 * Matches Ralph loop start command or announcement
 * Examples: "/ralph-loop:ralph-loop", "Starting Ralph Wiggum loop", "ralph loop beginning"
 */
const RALPH_START_PATTERN = /\/ralph-loop|starting ralph(?:\s+wiggum)?\s+loop|ralph loop (?:started|beginning)/i;
/**
 * Matches max iterations configuration
 * Examples: "max-iterations 50", "maxIterations: 50", "max_iterations=50"
 * Capture group 1: Maximum iteration count
 */
const MAX_ITERATIONS_PATTERN = /max[_-]?iterations?\s*[=:]\s*(\d+)/i;
/**
 * Matches TodoWrite tool usage indicators
 * Examples: "TodoWrite", "todos updated", "Todos have been modified"
 */
const TODOWRITE_PATTERN = /TodoWrite|todo(?:s)?\s*(?:updated|written|saved)|Todos have been modified/i;
// ---------- Task Completion Detection Patterns ----------
/**
 * Matches "all tasks complete" announcements
 * Examples: "All 8 files have been created", "All tasks completed", "Everything is done"
 * Used to mark all tracked todos as complete at once
 */
const ALL_COMPLETE_PATTERN = /all\s+(?:\d+\s+)?(?:tasks?|files?|items?)\s+(?:have\s+been\s+|are\s+)?(?:completed?|done|finished|created)|completed?\s+all\s+(?:\d+\s+)?tasks?|all\s+done|everything\s+(?:is\s+)?(?:completed?|done)|finished\s+all\s+tasks?/i;
/**
 * Extracts count from "all N items" messages
 * Example: "All 8 files created" → captures "8"
 * Capture group 1: The count
 */
const ALL_COUNT_PATTERN = /all\s+(\d+)\s+(?:tasks?|files?|items?)/i;
/**
 * Matches individual task completion messages
 * Examples: "Task #5 is done", "marked as completed", "todo 3 finished"
 * Used to update specific todo items by number
 */
const TASK_DONE_PATTERN = /(?:task|item|todo)\s*(?:#?\d+|"\s*[^"]+\s*")?\s*(?:is\s+)?(?:done|completed?|finished)|(?:completed?|done|finished)\s+(?:task|item)\s*(?:#?\d+)?|marking\s+(?:.*?\s+)?(?:as\s+)?completed?|marked\s+(?:.*?\s+)?(?:as\s+)?completed?/i;
// ---------- Utility Patterns ----------
/** Maximum number of task number to content mappings to track */
const MAX_TASK_MAPPINGS = 100;
// ---------- Priority Detection Patterns ----------
// Pre-compiled for performance; avoids repeated allocation in parsePriority()
/** P0 (Critical) priority patterns - highest severity issues */
const P0_PRIORITY_PATTERNS = [
    /\bP0\b|\(P0\)|:?\s*P0\s*:/, // Explicit P0
    /\bCRITICAL\b/, // Critical keyword
    /\bBLOCKER\b/, // Blocker
    /\bURGENT\b/, // Urgent
    /\bSECURITY\b/, // Security issues
    /\bCRASH(?:ES|ING)?\b/, // Crash, crashes, crashing
    /\bBROKEN\b/, // Broken
    /\bDATA\s*LOSS\b/, // Data loss
    /\bPRODUCTION\s*(?:DOWN|ISSUE|BUG)\b/, // Production issues
    /\bHOTFIX\b/, // Hotfix
    /\bSEVERITY\s*1\b/, // Severity 1
];
/** P1 (High) priority patterns - important issues requiring attention */
const P1_PRIORITY_PATTERNS = [
    /\bP1\b|\(P1\)|:?\s*P1\s*:/, // Explicit P1
    /\bHIGH\s*PRIORITY\b/, // High priority
    /\bIMPORTANT\b/, // Important
    /\bBUG\b/, // Bug
    /\bFIX\b/, // Fix (as task type)
    /\bERROR\b/, // Error
    /\bFAIL(?:S|ED|ING|URE)?\b/, // Fail variants
    /\bREGRESSION\b/, // Regression
    /\bMUST\s*(?:HAVE|FIX|DO)\b/, // Must have/fix/do
    /\bSEVERITY\s*2\b/, // Severity 2
    /\bREQUIRED\b/, // Required
];
/** P2 (Medium) priority patterns - lower priority improvements */
const P2_PRIORITY_PATTERNS = [
    /\bP2\b|\(P2\)|:?\s*P2\s*:/, // Explicit P2
    /\bNICE\s*TO\s*HAVE\b/, // Nice to have
    /\bLOW\s*PRIORITY\b/, // Low priority
    /\bREFACTOR\b/, // Refactor
    /\bCLEANUP\b/, // Cleanup
    /\bIMPROVE(?:MENT)?\b/, // Improve/Improvement
    /\bOPTIMIZ(?:E|ATION)\b/, // Optimize/Optimization
    /\bCONSIDER\b/, // Consider
    /\bWOULD\s*BE\s*NICE\b/, // Would be nice
    /\bENHANCE(?:MENT)?\b/, // Enhance/Enhancement
    /\bTECH(?:NICAL)?\s*DEBT\b/, // Tech debt
    /\bDOCUMENT(?:ATION)?\b/, // Documentation
];
// ========== Event Types ==========
/**
 * Events emitted by RalphTracker
 * @event loopUpdate - Fired when loop state changes (active, iteration, completion phrase)
 * @event todoUpdate - Fired when todo list changes (items added, status changed)
 * @event completionDetected - Fired when completion phrase is detected (task complete)
 * @event enabled - Fired when tracker auto-enables due to Ralph pattern detection
 * @event statusBlockDetected - Fired when a RALPH_STATUS block is parsed
 * @event circuitBreakerUpdate - Fired when circuit breaker state changes
 * @event exitGateMet - Fired when dual-condition exit gate is met
 */
/**
 * RalphTracker - Parses terminal output to detect Ralph Wiggum loops and todos
 *
 * This class monitors Claude Code session output to detect:
 * 1. **Ralph Wiggum loop state** - Active loops, completion phrases, iteration counts
 * 2. **Todo list items** - From TodoWrite tool in various formats
 * 3. **Completion signals** - `<promise>PHRASE</promise>` tags
 *
 * ## Lifecycle
 *
 * The tracker is **DISABLED by default** and auto-enables when Ralph-related
 * patterns are detected (e.g., /ralph-loop:ralph-loop, <promise>, todos).
 * This reduces overhead for sessions not using autonomous loops.
 *
 * ## Completion Detection
 *
 * Uses occurrence-based detection to distinguish prompt from actual completion:
 * - 1st occurrence of `<promise>X</promise>`: Stored as expected phrase (likely in prompt)
 * - 2nd occurrence: Emits `completionDetected` event (actual completion)
 * - If loop already active: Emits immediately on first occurrence
 *
 * ## Sub-modules
 *
 * - `planTracker` - Plan task management, checkpoints, versioning
 * - `fixPlanWatcher` - @fix_plan.md file watching and parsing
 * - `stallDetector` - Iteration stall detection
 * - `statusParser` - RALPH_STATUS block parsing, circuit breaker
 *
 * ## Events
 *
 * - `loopUpdate` - Loop state changed (status, iteration, phrase)
 * - `todoUpdate` - Todo list modified (add, status change)
 * - `completionDetected` - Loop completion phrase detected
 * - `enabled` - Tracker auto-enabled from disabled state
 *
 * @extends EventEmitter
 * @example
 * ```typescript
 * const tracker = new RalphTracker();
 * tracker.on('completionDetected', (phrase) => {
 *   console.log('Loop completed with phrase:', phrase);
 * });
 * tracker.processTerminalData(ptyOutput);
 * ```
 */
export class RalphTracker extends EventEmitter {
    // ========== Sub-modules ==========
    /** Plan task management sub-module */
    planTracker = new RalphPlanTracker();
    /** @fix_plan.md file watcher sub-module */
    fixPlanWatcher;
    /** Iteration stall detector sub-module */
    stallDetector = new RalphStallDetector();
    /** RALPH_STATUS block parser and circuit breaker sub-module */
    statusParser = new RalphStatusParser();
    // ========== Core State ==========
    /** Current state of the detected loop */
    _loopState;
    /** Map of todo items by ID for O(1) lookup */
    _todos = new Map();
    /** Buffer for incomplete lines from terminal data */
    _lineBuffer = '';
    /**
     * Tracks occurrences of completion phrases.
     * Used to distinguish prompt echo (1st) from actual completion (2nd+).
     */
    _completionPhraseCount = new Map();
    /** Timestamp of last cleanup check for throttling */
    _lastCleanupTime = 0;
    /** Debouncer for todoUpdate events */
    _todoDeb = new Debouncer(EVENT_DEBOUNCE_MS);
    /** Debouncer for loopUpdate events */
    _loopDeb = new Debouncer(EVENT_DEBOUNCE_MS);
    /** When true, prevents auto-enable on pattern detection */
    _autoEnableDisabled = true;
    /** Maps task numbers from "✔ Task #N" format to their content for status updates */
    _taskNumberToContent = new Map();
    /**
     * Buffer for partial promise tags split across PTY chunks.
     * Holds content after '<promise>' when closing tag hasn't arrived yet.
     * Max 256 chars to prevent unbounded growth from malformed tags.
     */
    _partialPromiseBuffer = '';
    /** Maximum size of partial promise buffer */
    static MAX_PARTIAL_PROMISE_SIZE = 256;
    /** Alternate completion phrases (P1-003: multi-phrase support) - Set for O(1) lookup */
    _alternateCompletionPhrases = new Set();
    // ========== P1-009: Progress Estimation ==========
    /** History of todo completion times (ms) for averaging */
    _completionTimes = [];
    /** Maximum number of completion times to track */
    static MAX_COMPLETION_TIMES = 50;
    /** Timestamp when todos started being tracked for this session */
    _todosStartedAt = 0;
    /** Map of todo ID to timestamp when it started (for duration tracking) */
    _todoStartTimes = new Map();
    /** Last calculated completion confidence */
    _lastCompletionConfidence;
    /** Manages periodic cleanup timers (todo expiry) */
    cleanup = new CleanupManager();
    /** Confidence threshold for triggering completion (0-100) */
    static COMPLETION_CONFIDENCE_THRESHOLD = 70;
    /**
     * Creates a new RalphTracker instance.
     * Starts in disabled state until Ralph patterns are detected.
     */
    constructor() {
        super();
        this._loopState = createInitialRalphTrackerState();
        // Initialize fix plan watcher with callbacks to parent methods
        this.fixPlanWatcher = new RalphFixPlanWatcher((content) => this.parsePriority(content), (content) => this.generateTodoId(content));
        // Wire sub-module events
        this._wireSubModuleEvents();
        // Periodic cleanup of expired todos — ensures stale entries are purged
        // even when no terminal data is flowing (e.g., idle sessions)
        this.cleanup.setInterval(() => this.cleanupExpiredTodos(), TODO_CLEANUP_INTERVAL_MS, {
            description: 'ralph todo expiry cleanup',
        });
    }
    /**
     * Forward all sub-module events through RalphTracker
     * so external consumers don't need to know about the split.
     */
    _wireSubModuleEvents() {
        // Forward plan tracker events
        for (const event of [
            'planInitialized',
            'planTaskUpdate',
            'taskBlocked',
            'taskUnblocked',
            'planCheckpoint',
            'planTaskAdded',
            'planRollback',
        ]) {
            this.planTracker.on(event, (...args) => this.emit(event, ...args));
        }
        // Forward status parser events
        this.statusParser.on('statusBlockDetected', (block) => {
            // Auto-enable tracker when we see a status block
            if (!this._loopState.enabled && !this._autoEnableDisabled) {
                this.enable();
            }
            this._loopState.lastActivity = Date.now();
            this.emit('statusBlockDetected', block);
            this.emitLoopUpdateDebounced();
        });
        this.statusParser.on('circuitBreakerUpdate', (status) => {
            this.emit('circuitBreakerUpdate', status);
        });
        this.statusParser.on('exitGateMet', (data) => {
            this.emit('exitGateMet', data);
        });
        // Forward stall detector events
        this.stallDetector.on('iterationStallWarning', (data) => {
            this.emit('iterationStallWarning', data);
        });
        this.stallDetector.on('iterationStallCritical', (data) => {
            this.emit('iterationStallCritical', data);
        });
        // Forward fix plan watcher events
        this.fixPlanWatcher.on('todosLoaded', (items) => {
            // Replace _todos with file-based items
            this._todos.clear();
            for (const item of items) {
                this._todos.set(item.id, item);
            }
            // Auto-enable tracker when we have todos from @fix_plan.md
            if (!this._loopState.enabled) {
                this.enable();
            }
            this.emit('todoUpdate', this.todos);
        });
    }
    // ========== Delegated Plan Tracker Methods ==========
    /**
     * Initialize plan tasks from generated plan items.
     */
    initializePlanTasks(items) {
        this.planTracker.initializePlanTasks(items);
    }
    /**
     * Update a specific plan task's status, attempts, or error.
     */
    updatePlanTask(taskId, update) {
        return this.planTracker.updatePlanTask(taskId, update);
    }
    /**
     * Add a new task to the plan.
     */
    addPlanTask(task) {
        return this.planTracker.addPlanTask(task);
    }
    /**
     * Get all plan tasks.
     */
    getPlanTasks() {
        return this.planTracker.getPlanTasks();
    }
    /**
     * Generate a checkpoint review.
     */
    generateCheckpointReview() {
        return this.planTracker.generateCheckpointReview();
    }
    /**
     * Get plan version history.
     */
    getPlanHistory() {
        return this.planTracker.getPlanHistory();
    }
    /**
     * Rollback to a previous plan version.
     */
    rollbackToVersion(version) {
        return this.planTracker.rollbackToVersion(version);
    }
    /**
     * Check if checkpoint review is due.
     */
    isCheckpointDue() {
        return this.planTracker.isCheckpointDue();
    }
    /**
     * Get current plan version.
     */
    get planVersion() {
        return this.planTracker.planVersion;
    }
    // ========== Delegated Fix Plan Watcher Methods ==========
    /**
     * Set the working directory and start watching @fix_plan.md.
     * @param workingDir - The session's working directory
     */
    setWorkingDir(workingDir) {
        this.fixPlanWatcher.setWorkingDir(workingDir);
    }
    /**
     * Load @fix_plan.md from disk if it exists.
     */
    async loadFixPlanFromDisk() {
        return this.fixPlanWatcher.loadFixPlanFromDisk();
    }
    /**
     * Stop watching @fix_plan.md.
     */
    stopWatchingFixPlan() {
        this.fixPlanWatcher.stopWatchingFixPlan();
    }
    /**
     * When @fix_plan.md is active, treat it as the source of truth for todo status.
     */
    get isFileAuthoritative() {
        return this.fixPlanWatcher.isFileAuthoritative;
    }
    /**
     * Generate @fix_plan.md content from current todos.
     */
    generateFixPlanMarkdown() {
        return generateFixPlanMarkdown(this.todos);
    }
    /**
     * Parse @fix_plan.md content and import todos.
     * Replaces current todos with imported ones.
     *
     * @param content - Markdown content from @fix_plan.md
     * @returns Number of todos imported
     */
    importFixPlanMarkdown(content) {
        const newTodos = importFixPlanMarkdown(content, (c) => this.parsePriority(c), (c) => this.generateTodoId(c));
        // Replace current todos with imported ones
        this._todos.clear();
        for (const todo of newTodos) {
            this._todos.set(todo.id, todo);
        }
        // Emit update
        this.emit('todoUpdate', this.todos);
        return newTodos.length;
    }
    // ========== Delegated Stall Detector Methods ==========
    /**
     * Start iteration stall detection timer.
     */
    startIterationStallDetection() {
        this.stallDetector.startIterationStallDetection();
    }
    /**
     * Stop iteration stall detection timer.
     */
    stopIterationStallDetection() {
        this.stallDetector.stopIterationStallDetection();
    }
    /**
     * Get iteration stall metrics for monitoring.
     */
    getIterationStallMetrics() {
        return this.stallDetector.getIterationStallMetrics();
    }
    /**
     * Configure iteration stall thresholds.
     */
    configureIterationStallThresholds(warningMs, criticalMs) {
        this.stallDetector.configureIterationStallThresholds(warningMs, criticalMs);
    }
    // ========== Delegated Status Parser Methods ==========
    /**
     * Manually reset circuit breaker to CLOSED state.
     * @fires circuitBreakerUpdate
     */
    resetCircuitBreaker() {
        this.statusParser.resetCircuitBreaker();
    }
    /**
     * Get current circuit breaker status.
     */
    get circuitBreakerStatus() {
        return this.statusParser.circuitBreakerStatus;
    }
    /**
     * Get last parsed RALPH_STATUS block.
     */
    get lastStatusBlock() {
        return this.statusParser.lastStatusBlock;
    }
    /**
     * Get cumulative stats from status blocks.
     */
    get cumulativeStats() {
        return this.statusParser.cumulativeStats;
    }
    /**
     * Whether dual-condition exit gate has been met.
     */
    get exitGateMet() {
        return this.statusParser.exitGateMet;
    }
    // ========== Core Methods ==========
    /**
     * Add an alternate completion phrase (P1-003: multi-phrase support).
     */
    addAlternateCompletionPhrase(phrase) {
        if (!this._alternateCompletionPhrases.has(phrase)) {
            this._alternateCompletionPhrases.add(phrase);
            this._loopState.alternateCompletionPhrases = Array.from(this._alternateCompletionPhrases);
            this.emit('loopUpdate', this.loopState);
        }
    }
    /**
     * Remove an alternate completion phrase.
     */
    removeAlternateCompletionPhrase(phrase) {
        if (this._alternateCompletionPhrases.delete(phrase)) {
            this._loopState.alternateCompletionPhrases = Array.from(this._alternateCompletionPhrases);
            this.emit('loopUpdate', this.loopState);
        }
    }
    /**
     * Check if a phrase matches any valid completion phrase (primary or alternate).
     */
    isValidCompletionPhrase(phrase) {
        return this.findMatchingCompletionPhrase(phrase) !== null;
    }
    /**
     * Find which completion phrase (primary or alternate) matches the given phrase.
     */
    findMatchingCompletionPhrase(phrase) {
        const primary = this._loopState.completionPhrase;
        if (primary && this.isFuzzyPhraseMatch(phrase, primary)) {
            return primary;
        }
        for (const alt of this._alternateCompletionPhrases) {
            if (this.isFuzzyPhraseMatch(phrase, alt)) {
                return alt;
            }
        }
        return null;
    }
    /**
     * Prevent auto-enable from pattern detection.
     */
    disableAutoEnable() {
        this._autoEnableDisabled = true;
    }
    /**
     * Allow auto-enable from pattern detection.
     */
    enableAutoEnable() {
        this._autoEnableDisabled = false;
    }
    /**
     * Whether auto-enable is disabled.
     */
    get autoEnableDisabled() {
        return this._autoEnableDisabled;
    }
    /**
     * Whether the tracker is enabled and actively monitoring output.
     */
    get enabled() {
        return this._loopState.enabled;
    }
    /**
     * Enable the tracker to start monitoring terminal output.
     * @fires enabled
     * @fires loopUpdate
     */
    enable() {
        if (!this._loopState.enabled) {
            this._loopState.enabled = true;
            this._loopState.lastActivity = Date.now();
            this.emit('enabled');
            this.emit('loopUpdate', this.loopState);
        }
    }
    /**
     * Disable the tracker to stop monitoring terminal output.
     * @fires loopUpdate
     */
    disable() {
        if (this._loopState.enabled) {
            this._loopState.enabled = false;
            this._loopState.lastActivity = Date.now();
            this.emit('loopUpdate', this.loopState);
        }
    }
    /**
     * Soft reset - clears state but keeps enabled status.
     * @fires loopUpdate
     * @fires todoUpdate
     */
    reset() {
        // Clear debounce timers
        this.clearDebounceTimers();
        const wasEnabled = this._loopState.enabled;
        this._loopState = createInitialRalphTrackerState();
        this._loopState.enabled = wasEnabled; // Keep enabled status
        this._todos.clear();
        this._completionPhraseCount.clear();
        this._taskNumberToContent.clear();
        this._lineBuffer = '';
        this._partialPromiseBuffer = '';
        // Reset sub-modules
        this.statusParser.reset();
        this.planTracker.reset();
        this.stallDetector.reset();
        // Emit on next tick to prevent listeners from modifying state during reset (non-reentrant)
        const loopState = this.loopState;
        const todos = this.todos;
        process.nextTick(() => {
            this.emit('loopUpdate', loopState);
            this.emit('todoUpdate', todos);
        });
    }
    /**
     * Full reset - clears all state including enabled status.
     * @fires loopUpdate
     * @fires todoUpdate
     */
    fullReset() {
        // Clear debounce timers
        this.clearDebounceTimers();
        this._loopState = createInitialRalphTrackerState();
        this._todos.clear();
        this._completionPhraseCount.clear();
        this._taskNumberToContent.clear();
        this._todoStartTimes.clear();
        this._alternateCompletionPhrases.clear();
        this._lineBuffer = '';
        this._partialPromiseBuffer = '';
        // Full reset sub-modules
        this.statusParser.fullReset();
        this.planTracker.fullReset();
        this.stallDetector.reset();
        // Emit on next tick to prevent listeners from modifying state during reset (non-reentrant)
        const loopState = this.loopState;
        const todos = this.todos;
        process.nextTick(() => {
            this.emit('loopUpdate', loopState);
            this.emit('todoUpdate', todos);
        });
    }
    /**
     * Clear all debounce timers.
     */
    clearDebounceTimers() {
        this._todoDeb.cancel();
        this._loopDeb.cancel();
    }
    /**
     * Emit todoUpdate event with debouncing.
     */
    emitTodoUpdateDebounced() {
        this._todoDeb.schedule(() => this.emit('todoUpdate', this.todos));
    }
    /**
     * Emit loopUpdate event with debouncing.
     */
    emitLoopUpdateDebounced() {
        this._loopDeb.schedule(() => this.emit('loopUpdate', this.loopState));
    }
    /**
     * Flush all pending debounced events immediately.
     */
    flushPendingEvents() {
        if (this._todoDeb.isPending) {
            this._todoDeb.cancel();
            this.emit('todoUpdate', this.todos);
        }
        if (this._loopDeb.isPending) {
            this._loopDeb.cancel();
            this.emit('loopUpdate', this.loopState);
        }
    }
    /**
     * Get a copy of the current loop state.
     */
    get loopState() {
        return {
            ...this._loopState,
            planVersion: this.planTracker.planVersion,
            planHistoryLength: this.planTracker.getPlanHistory().length,
            completionConfidence: this._lastCompletionConfidence,
        };
    }
    /**
     * Calculate confidence score for a potential completion signal.
     */
    calculateCompletionConfidence(phrase, context) {
        let score = 0;
        const signals = {
            hasPromiseTag: false,
            matchesExpected: false,
            allTodosComplete: false,
            hasExitSignal: false,
            multipleIndicators: false,
            contextAppropriate: true, // Default to true, deduct if inappropriate
        };
        // Check for promise tag format (adds 30 points)
        if (context && PROMISE_PATTERN.test(context)) {
            signals.hasPromiseTag = true;
            score += 30;
        }
        // Check if phrase matches expected completion phrase (adds 25 points)
        const expectedPhrase = this._loopState.completionPhrase;
        if (expectedPhrase) {
            const matchedPhrase = this.findMatchingCompletionPhrase(phrase);
            if (matchedPhrase) {
                signals.matchesExpected = true;
                score += 25;
            }
        }
        // Check if all todos are complete (adds 20 points)
        const todoArray = Array.from(this._todos.values());
        if (todoArray.length > 0 && todoArray.every((t) => t.status === 'completed')) {
            signals.allTodosComplete = true;
            score += 20;
        }
        // Check for EXIT_SIGNAL from RALPH_STATUS block (adds 15 points)
        const lastBlock = this.statusParser.lastStatusBlock;
        if (lastBlock?.exitSignal === true) {
            signals.hasExitSignal = true;
            score += 15;
        }
        // Check for multiple completion indicators (adds 10 points)
        if (this.statusParser.cumulativeStats.completionIndicators >= 2) {
            signals.multipleIndicators = true;
            score += 10;
        }
        // Check context appropriateness (deduct if inappropriate)
        if (context) {
            const lowerContext = context.toLowerCase();
            if (lowerContext.includes('output:') ||
                lowerContext.includes('completion phrase') ||
                lowerContext.includes('output exactly') ||
                lowerContext.includes('when done')) {
                signals.contextAppropriate = false;
                score -= 20;
            }
            else {
                score += 10;
            }
        }
        // Bonus for active loop state (adds 10 points)
        if (this._loopState.active) {
            score += 10;
        }
        // Bonus for 2nd+ occurrence (adds 15 points)
        const count = this._completionPhraseCount.get(phrase) || 0;
        if (count >= 2) {
            score += 15;
        }
        // Clamp score to 0-100
        score = Math.max(0, Math.min(100, score));
        const confidence = {
            score,
            isConfident: score >= RalphTracker.COMPLETION_CONFIDENCE_THRESHOLD,
            signals,
            calculatedAt: Date.now(),
        };
        this._lastCompletionConfidence = confidence;
        return confidence;
    }
    /**
     * Get all tracked todo items as an array.
     */
    get todos() {
        return Array.from(this._todos.values());
    }
    /**
     * Process raw terminal data to detect inner loop patterns.
     */
    processTerminalData(data) {
        // Remove ANSI escape codes for cleaner parsing
        const cleanData = data.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '');
        this.processCleanData(cleanData);
    }
    /**
     * Process pre-stripped terminal data (ANSI codes already removed).
     */
    processCleanData(cleanData) {
        // If tracker is disabled, only check for patterns that should auto-enable it
        if (!this._loopState.enabled) {
            if (this._autoEnableDisabled) {
                return;
            }
            if (this.shouldAutoEnable(cleanData)) {
                this.enable();
            }
            else {
                return;
            }
        }
        // Buffer data for line-based processing
        this._lineBuffer += cleanData;
        // Prevent unbounded line buffer growth from very long lines
        if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {
            this._lineBuffer = this._lineBuffer.slice(-Math.floor(MAX_LINE_BUFFER_SIZE / 2));
        }
        // Process complete lines
        const lines = this._lineBuffer.split('\n');
        this._lineBuffer = lines.pop() || '';
        for (const line of lines) {
            this.processLine(line);
        }
        // Also check the current buffer for multi-line patterns
        this.checkMultiLinePatterns(cleanData);
        // Cleanup expired todos (throttled to avoid running on every chunk)
        this.maybeCleanupExpiredTodos();
    }
    /**
     * Check if data contains patterns that should auto-enable the tracker.
     */
    shouldAutoEnable(data) {
        // Cheap pre-filter: skip the full regex battery if none of the key
        // substrings that any pattern could match are present in the data.
        if (!data.includes('<') &&
            !data.includes('ralph') &&
            !data.includes('Ralph') &&
            !data.includes('Todo') &&
            !data.includes('todo') &&
            !data.includes('Iteration') &&
            !data.includes('[') &&
            !data.includes('\u2610') &&
            !data.includes('\u2612') &&
            !data.includes('\u2714') &&
            !data.includes('Loop') &&
            !data.includes('complete') &&
            !data.includes('COMPLETE') &&
            !data.includes('Done') &&
            !data.includes('DONE')) {
            return false;
        }
        if (RALPH_START_PATTERN.test(data))
            return true;
        if (PROMISE_PATTERN.test(data))
            return true;
        if (TODOWRITE_PATTERN.test(data))
            return true;
        if (ITERATION_PATTERN.test(data))
            return true;
        TODO_CHECKBOX_PATTERN.lastIndex = 0;
        if (TODO_CHECKBOX_PATTERN.test(data))
            return true;
        TODO_INDICATOR_PATTERN.lastIndex = 0;
        if (TODO_INDICATOR_PATTERN.test(data))
            return true;
        TODO_NATIVE_PATTERN.lastIndex = 0;
        if (TODO_NATIVE_PATTERN.test(data))
            return true;
        TODO_TASK_CREATED_PATTERN.lastIndex = 0;
        if (TODO_TASK_CREATED_PATTERN.test(data))
            return true;
        TODO_TASK_STATUS_PATTERN.lastIndex = 0;
        if (TODO_TASK_STATUS_PATTERN.test(data))
            return true;
        if (LOOP_START_PATTERN.test(data))
            return true;
        if (ALL_COMPLETE_PATTERN.test(data))
            return true;
        if (TASK_DONE_PATTERN.test(data))
            return true;
        return false;
    }
    /**
     * Process a single line of terminal output.
     */
    processLine(line) {
        const trimmed = line.trim();
        if (!trimmed)
            return;
        // Delegate RALPH_STATUS block and completion indicator detection to sub-module
        this.statusParser.processLine(trimmed);
        // Check for completion phrase
        this.detectCompletionPhrase(trimmed);
        // Check for "all tasks complete" signals
        this.detectAllTasksComplete(trimmed);
        // Check for individual task completion signals
        this.detectTaskCompletion(trimmed);
        // Check for loop start/status
        this.detectLoopStatus(trimmed);
        // Check for todo items
        this.detectTodoItems(trimmed);
    }
    /**
     * Mark all tracked todos as completed and emit todoUpdate if any changed.
     * @returns true if any todo was updated
     */
    completeAllTodos() {
        let updated = false;
        for (const todo of this._todos.values()) {
            if (todo.status !== 'completed') {
                todo.status = 'completed';
                updated = true;
            }
        }
        if (updated) {
            this.emit('todoUpdate', this.todos);
        }
        return updated;
    }
    /**
     * Detect "all tasks complete" messages.
     */
    detectAllTasksComplete(line) {
        if (this.isFileAuthoritative)
            return;
        if (!ALL_COMPLETE_PATTERN.test(line))
            return;
        if (line.length > 100)
            return;
        if (line.toLowerCase().includes('output:') || line.includes('<promise>'))
            return;
        if (this._todos.size === 0)
            return;
        const countMatch = line.match(ALL_COUNT_PATTERN);
        const parsedCount = countMatch ? parseInt(countMatch[1], 10) : NaN;
        const mentionedCount = Number.isNaN(parsedCount) ? null : parsedCount;
        const todoCount = this._todos.size;
        if (mentionedCount !== null && Math.abs(mentionedCount - todoCount) > 2) {
            return;
        }
        this.completeAllTodos();
        if (this._loopState.completionPhrase) {
            this._loopState.active = false;
            this._loopState.lastActivity = Date.now();
            this.emit('completionDetected', this._loopState.completionPhrase);
            this.emit('loopUpdate', this.loopState);
        }
    }
    /**
     * Detect individual task completion signals.
     */
    detectTaskCompletion(line) {
        if (this.isFileAuthoritative)
            return;
        if (!TASK_DONE_PATTERN.test(line))
            return;
        const taskNumMatch = line.match(/task\s*#?(\d+)/i);
        if (taskNumMatch) {
            const taskNum = parseInt(taskNumMatch[1], 10);
            if (Number.isNaN(taskNum))
                return;
            let count = 0;
            for (const [_id, todo] of this._todos) {
                count++;
                if (count === taskNum && todo.status !== 'completed') {
                    todo.status = 'completed';
                    this.emit('todoUpdate', this.todos);
                    break;
                }
            }
        }
    }
    /**
     * Check for multi-line patterns that might span line boundaries.
     */
    checkMultiLinePatterns(data) {
        if (this._partialPromiseBuffer) {
            const combinedData = this._partialPromiseBuffer + data;
            const promiseMatch = combinedData.match(PROMISE_PATTERN);
            if (promiseMatch) {
                const phrase = promiseMatch[1].trim();
                this._partialPromiseBuffer = '';
                this.handleCompletionPhrase(phrase);
                return;
            }
        }
        const partialMatch = data.match(PROMISE_PARTIAL_PATTERN);
        if (partialMatch) {
            const partialContent = partialMatch[0];
            if (partialContent.length <= RalphTracker.MAX_PARTIAL_PROMISE_SIZE) {
                this._partialPromiseBuffer = partialContent;
            }
            else {
                this._partialPromiseBuffer = '';
            }
        }
        else {
            this._partialPromiseBuffer = '';
        }
    }
    /**
     * Detect completion phrases in a line.
     */
    detectCompletionPhrase(line) {
        const match = line.match(PROMISE_PATTERN);
        if (match) {
            this.handleCompletionPhrase(match[1]);
            return;
        }
        const expectedPhrase = this._loopState.completionPhrase;
        if (expectedPhrase && line.toUpperCase().includes(expectedPhrase.toUpperCase())) {
            const isNotInPromptContext = !line.includes('<promise>') && !line.includes('output:');
            const isNotExplanation = !line.toLowerCase().includes('completion phrase') && !line.toLowerCase().includes('output exactly');
            if (isNotInPromptContext && isNotExplanation) {
                this.handleBareCompletionPhrase(expectedPhrase);
            }
        }
    }
    /**
     * Handle a bare completion phrase (without XML tags).
     */
    handleBareCompletionPhrase(phrase) {
        const taggedCount = this._completionPhraseCount.get(phrase) || 0;
        const loopExplicitlyActive = this._loopState.active;
        if (taggedCount === 0 && !loopExplicitlyActive)
            return;
        const bareKey = `bare:${phrase}`;
        const bareCount = (this._completionPhraseCount.get(bareKey) || 0) + 1;
        this._completionPhraseCount.set(bareKey, bareCount);
        if (bareCount > 1)
            return;
        this.completeAllTodos();
        this._loopState.active = false;
        this._loopState.lastActivity = Date.now();
        this.emit('completionDetected', phrase);
        this.emit('loopUpdate', this.loopState);
    }
    /**
     * Handle a detected completion phrase.
     */
    handleCompletionPhrase(phrase) {
        const count = (this._completionPhraseCount.get(phrase) || 0) + 1;
        this._completionPhraseCount.set(phrase, count);
        // Trim completion phrase map if it exceeds the limit
        if (this._completionPhraseCount.size > MAX_COMPLETION_PHRASE_ENTRIES) {
            const entries = Array.from(this._completionPhraseCount.entries());
            entries.sort((a, b) => b[1] - a[1]);
            this._completionPhraseCount.clear();
            const keepCount = Math.floor(MAX_COMPLETION_PHRASE_ENTRIES / 2);
            for (let i = 0; i < Math.min(keepCount, entries.length); i++) {
                this._completionPhraseCount.set(entries[i][0], entries[i][1]);
            }
            if (this._loopState.completionPhrase && !this._completionPhraseCount.has(this._loopState.completionPhrase)) {
                this._completionPhraseCount.set(this._loopState.completionPhrase, 1);
            }
        }
        // Store phrase on first occurrence
        if (!this._loopState.completionPhrase) {
            this._loopState.completionPhrase = phrase;
            this._loopState.lastActivity = Date.now();
            this.validateCompletionPhrase(phrase);
            this.emit('loopUpdate', this.loopState);
        }
        // Check for fuzzy match with primary phrase or any alternate phrase
        const matchedPhrase = this.findMatchingCompletionPhrase(phrase);
        if (matchedPhrase) {
            const canonicalCount = this._completionPhraseCount.get(matchedPhrase) || 0;
            if (canonicalCount >= 2 || this._loopState.active) {
                this._loopState.active = false;
                this._loopState.lastActivity = Date.now();
                this.completeAllTodos();
                this.emit('completionDetected', matchedPhrase);
                this.emit('loopUpdate', this.loopState);
                return;
            }
        }
        if (this._loopState.active || count >= 2) {
            this.completeAllTodos();
            this._loopState.active = false;
            this._loopState.lastActivity = Date.now();
            this.emit('completionDetected', phrase);
            this.emit('loopUpdate', this.loopState);
        }
    }
    /**
     * Check if two phrases match with fuzzy tolerance.
     */
    isFuzzyPhraseMatch(phrase1, phrase2, maxDistance = 2) {
        return fuzzyPhraseMatch(phrase1, phrase2, maxDistance);
    }
    /**
     * Validate a completion phrase and emit warnings if it's risky.
     */
    validateCompletionPhrase(phrase) {
        const normalized = phrase.toUpperCase().replace(/[\s_\-.]+/g, '');
        const uniqueSuffix = Date.now().toString(36).slice(-4).toUpperCase();
        const suggestedPhrase = `${phrase}_${uniqueSuffix}`;
        if (COMMON_COMPLETION_PHRASES.has(normalized)) {
            this.emitValidationWarning(phrase, 'common', suggestedPhrase);
            return;
        }
        if (normalized.length < MIN_RECOMMENDED_PHRASE_LENGTH) {
            this.emitValidationWarning(phrase, 'short', suggestedPhrase);
            return;
        }
        if (/^\d+$/.test(normalized)) {
            this.emitValidationWarning(phrase, 'numeric', suggestedPhrase);
        }
    }
    /**
     * Emit a phrase validation warning with a console message and event.
     */
    emitValidationWarning(phrase, reason, suggestedPhrase) {
        const descriptions = {
            common: 'is very common and may cause false positives',
            short: `is too short (${phrase.toUpperCase().replace(/[\s_\-.]+/g, '').length} chars)`,
            numeric: 'is numeric-only and may cause false positives',
        };
        console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" ${descriptions[reason]}. Consider using: "${suggestedPhrase}"`);
        this.emit('phraseValidationWarning', {
            phrase,
            reason,
            suggestedPhrase,
        });
    }
    /**
     * Activate the loop if not already active.
     */
    activateLoopIfNeeded() {
        if (this._loopState.active)
            return false;
        this._loopState.active = true;
        this._loopState.startedAt = Date.now();
        this._loopState.cycleCount = 0;
        this._loopState.maxIterations = null;
        this._loopState.elapsedHours = null;
        this._loopState.lastActivity = Date.now();
        this.emit('loopUpdate', this.loopState);
        return true;
    }
    /**
     * Detect loop start and status indicators.
     */
    detectLoopStatus(line) {
        if (RALPH_START_PATTERN.test(line) || LOOP_START_PATTERN.test(line)) {
            this.activateLoopIfNeeded();
        }
        const maxIterMatch = line.match(MAX_ITERATIONS_PATTERN);
        if (maxIterMatch) {
            const maxIter = parseInt(maxIterMatch[1], 10);
            if (!Number.isNaN(maxIter) && maxIter > 0) {
                this._loopState.maxIterations = maxIter;
                this._loopState.lastActivity = Date.now();
                this.emitLoopUpdateDebounced();
            }
        }
        const iterMatch = line.match(ITERATION_PATTERN);
        if (iterMatch) {
            const currentIter = parseInt(iterMatch[1] || iterMatch[3], 10);
            const maxIterStr = iterMatch[2] || iterMatch[4];
            const maxIter = maxIterStr ? parseInt(maxIterStr, 10) : null;
            if (!Number.isNaN(currentIter)) {
                this.activateLoopIfNeeded();
                // Track iteration changes for stall detection and circuit breaker
                if (currentIter !== this.stallDetector.getIterationStallMetrics().currentIteration) {
                    this.stallDetector.notifyIterationChanged(currentIter);
                    this.statusParser.notifyIterationProgress(currentIter);
                }
                this._loopState.cycleCount = currentIter;
                // Notify sub-modules of cycle count
                this.planTracker.notifyCycleCount(currentIter);
                this.statusParser.setCycleCount(currentIter);
                this.stallDetector.setLoopActive(true);
                if (maxIter !== null && !Number.isNaN(maxIter)) {
                    this._loopState.maxIterations = maxIter;
                }
                this._loopState.lastActivity = Date.now();
                this.emitLoopUpdateDebounced();
            }
        }
        const elapsedMatch = line.match(ELAPSED_TIME_PATTERN);
        if (elapsedMatch) {
            this._loopState.elapsedHours = parseFloat(elapsedMatch[1]);
            this._loopState.lastActivity = Date.now();
            this.emitLoopUpdateDebounced();
        }
        const cycleMatch = line.match(CYCLE_PATTERN);
        if (cycleMatch) {
            const cycleNum = parseInt(cycleMatch[1] || cycleMatch[2], 10);
            if (!Number.isNaN(cycleNum) && cycleNum > this._loopState.cycleCount) {
                this._loopState.cycleCount = cycleNum;
                this._loopState.lastActivity = Date.now();
                this.emitLoopUpdateDebounced();
            }
        }
        if (TODOWRITE_PATTERN.test(line)) {
            this._loopState.lastActivity = Date.now();
        }
    }
    /**
     * Detect todo items in various formats from Claude Code output.
     */
    detectTodoItems(line) {
        const hasCheckbox = line.includes('[');
        const hasTodoIndicator = line.includes('Todo:');
        const hasNativeCheckbox = line.includes('☐') || line.includes('☒') || line.includes('◐') || line.includes('✓');
        const hasStatus = line.includes('(pending)') || line.includes('(in_progress)') || line.includes('(completed)');
        const hasCheckmark = line.includes('✔');
        if (!hasCheckbox && !hasTodoIndicator && !hasNativeCheckbox && !hasStatus && !hasCheckmark) {
            return;
        }
        let updated = false;
        let match;
        if (hasCheckbox) {
            execPattern(TODO_CHECKBOX_PATTERN, line, (match) => {
                const checked = match[1].toLowerCase() === 'x';
                const content = match[2].trim();
                const status = checked ? 'completed' : 'pending';
                this.upsertTodo(content, status);
                updated = true;
            });
        }
        if (hasTodoIndicator) {
            execPattern(TODO_INDICATOR_PATTERN, line, (match) => {
                const icon = match[1];
                const content = match[2].trim();
                const status = this.iconToStatus(icon);
                this.upsertTodo(content, status);
                updated = true;
            });
        }
        if (hasStatus) {
            execPattern(TODO_STATUS_PATTERN, line, (match) => {
                const content = match[1].trim();
                const status = match[2];
                this.upsertTodo(content, status);
                updated = true;
            });
        }
        if (hasNativeCheckbox) {
            TODO_NATIVE_PATTERN.lastIndex = 0;
            while ((match = TODO_NATIVE_PATTERN.exec(line)) !== null) {
                const icon = match[1];
                const content = match[2].trim();
                const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
                if (shouldExclude)
                    continue;
                if (content.length < 5)
                    continue;
                const status = this.iconToStatus(icon);
                this.upsertTodo(content, status);
                updated = true;
            }
        }
        if (hasCheckmark) {
            execPattern(TODO_TASK_CREATED_PATTERN, line, (match) => {
                const taskNum = parseInt(match[1], 10);
                const content = match[2].trim();
                if (content.length >= 5) {
                    this._taskNumberToContent.set(taskNum, content);
                    this.enforceTaskMappingLimit();
                    this.upsertTodo(content, 'pending');
                    updated = true;
                }
            });
            execPattern(TODO_TASK_SUMMARY_PATTERN, line, (match) => {
                const taskNum = parseInt(match[1], 10);
                const content = match[2].trim();
                if (content.length >= 5) {
                    if (!this._taskNumberToContent.has(taskNum)) {
                        this._taskNumberToContent.set(taskNum, content);
                        this.enforceTaskMappingLimit();
                    }
                    this.upsertTodo(this._taskNumberToContent.get(taskNum) || content, 'pending');
                    updated = true;
                }
            });
            execPattern(TODO_TASK_STATUS_PATTERN, line, (match) => {
                const taskNum = parseInt(match[1], 10);
                const statusStr = match[2].trim();
                const status = statusStr === 'completed' ? 'completed' : statusStr === 'in progress' ? 'in_progress' : 'pending';
                const content = this._taskNumberToContent.get(taskNum);
                if (content) {
                    this.upsertTodo(content, status);
                    updated = true;
                }
            });
            if (!updated) {
                TODO_PLAIN_CHECKMARK_PATTERN.lastIndex = 0;
                while ((match = TODO_PLAIN_CHECKMARK_PATTERN.exec(line)) !== null) {
                    const content = match[1].trim();
                    const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
                    if (shouldExclude)
                        continue;
                    if (content.length < 5)
                        continue;
                    if (/^(Task\s*#\d+|#\d+)\s/.test(content))
                        continue;
                    this.upsertTodo(content, 'completed');
                    updated = true;
                }
            }
        }
        if (updated) {
            this.emitTodoUpdateDebounced();
        }
    }
    /**
     * Convert a todo icon character to its corresponding status.
     */
    iconToStatus(icon) {
        switch (icon) {
            case '✓':
            case '✅':
            case '☒':
            case '◉':
            case '●':
                return 'completed';
            case '◐':
            case '⏳':
            case '⌛':
            case '🔄':
                return 'in_progress';
            case '☐':
            case '○':
            default:
                return 'pending';
        }
    }
    /**
     * Parse priority from todo content.
     */
    parsePriority(content) {
        const upper = content.toUpperCase();
        for (const pattern of P0_PRIORITY_PATTERNS) {
            if (pattern.test(upper)) {
                return 'P0';
            }
        }
        for (const pattern of P1_PRIORITY_PATTERNS) {
            if (pattern.test(upper)) {
                return 'P1';
            }
        }
        for (const pattern of P2_PRIORITY_PATTERNS) {
            if (pattern.test(upper)) {
                return 'P2';
            }
        }
        return null;
    }
    /**
     * Add a new todo item or update an existing one.
     */
    upsertTodo(content, status) {
        if (!content || !content.trim())
            return;
        const cleanContent = content.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '').replace(/\s+/g, ' ').trim();
        if (cleanContent.length < 5)
            return;
        const priority = this.parsePriority(cleanContent);
        const estimatedComplexity = this.estimateComplexity(cleanContent);
        const id = this.generateTodoId(cleanContent);
        const existing = this._todos.get(id);
        if (existing) {
            const wasCompleted = existing.status === 'completed';
            const isNowCompleted = status === 'completed';
            const wasInProgress = existing.status === 'in_progress';
            const isNowInProgress = status === 'in_progress';
            existing.status = status;
            existing.detectedAt = Date.now();
            if (priority)
                existing.priority = priority;
            if (!existing.estimatedComplexity) {
                existing.estimatedComplexity = estimatedComplexity;
            }
            if (!wasCompleted && isNowCompleted) {
                this.recordTodoCompletion(id);
            }
            if (!wasInProgress && isNowInProgress) {
                this.startTrackingTodo(id);
            }
        }
        else {
            const similar = this.findSimilarTodo(cleanContent);
            if (similar) {
                const wasCompleted = similar.status === 'completed';
                const isNowCompleted = status === 'completed';
                const wasInProgress = similar.status === 'in_progress';
                const isNowInProgress = status === 'in_progress';
                similar.status = status;
                similar.detectedAt = Date.now();
                if (priority && !similar.priority) {
                    similar.priority = priority;
                }
                if (cleanContent.length > similar.content.length) {
                    similar.content = cleanContent;
                }
                if (!wasCompleted && isNowCompleted) {
                    this.recordTodoCompletion(similar.id);
                }
                if (!wasInProgress && isNowInProgress) {
                    this.startTrackingTodo(similar.id);
                }
                return;
            }
            while (this._todos.size >= MAX_TODOS_PER_SESSION) {
                const oldest = this.findOldestTodo();
                if (oldest) {
                    this._todos.delete(oldest.id);
                }
                else {
                    const firstKey = this._todos.keys().next().value;
                    if (firstKey)
                        this._todos.delete(firstKey);
                    else
                        break;
                }
            }
            const estimatedDurationMs = this.getEstimatedDuration(estimatedComplexity);
            this._todos.set(id, {
                id,
                content: cleanContent,
                status,
                detectedAt: Date.now(),
                priority,
                estimatedComplexity,
                estimatedDurationMs,
            });
            if (status === 'in_progress') {
                this.startTrackingTodo(id);
            }
        }
    }
    /**
     * Normalize todo content for consistent matching.
     */
    normalizeTodoContent(content) {
        if (!content)
            return '';
        return content
            .replace(/\s+/g, ' ')
            .replace(/[^a-zA-Z0-9\s.,!?'"-]/g, '')
            .trim()
            .toLowerCase();
    }
    /**
     * Calculate similarity between two strings.
     */
    calculateSimilarity(str1, str2) {
        const norm1 = this.normalizeTodoContent(str1);
        const norm2 = this.normalizeTodoContent(str2);
        if (norm1 === norm2)
            return 1.0;
        if (!norm1 || !norm2)
            return 0.0;
        const levenshteinSim = stringSimilarity(norm1, norm2);
        const bigramSim = this.calculateBigramSimilarity(norm1, norm2);
        return Math.max(levenshteinSim, bigramSim);
    }
    /**
     * Calculate bigram (Dice coefficient) similarity.
     */
    calculateBigramSimilarity(norm1, norm2) {
        if (norm1.length < 3 || norm2.length < 3) {
            const shorter = norm1.length <= norm2.length ? norm1 : norm2;
            const longer = norm1.length > norm2.length ? norm1 : norm2;
            return longer.includes(shorter) ? 0.9 : 0.0;
        }
        const getBigrams = (s) => {
            const bigrams = new Set();
            for (let i = 0; i < s.length - 1; i++) {
                bigrams.add(s.substring(i, i + 2));
            }
            return bigrams;
        };
        const bigrams1 = getBigrams(norm1);
        const bigrams2 = getBigrams(norm2);
        let intersection = 0;
        for (const bigram of bigrams1) {
            if (bigrams2.has(bigram)) {
                intersection++;
            }
        }
        const totalBigrams = bigrams1.size + bigrams2.size;
        if (totalBigrams === 0)
            return 0.0;
        return (2 * intersection) / totalBigrams;
    }
    /**
     * Find an existing todo that is similar to the given content.
     */
    findSimilarTodo(content) {
        const normalized = this.normalizeTodoContent(content);
        let threshold;
        if (normalized.length < 30) {
            threshold = SIMILARITY_THRESHOLD_SHORT;
        }
        else if (normalized.length < 60) {
            threshold = SIMILARITY_THRESHOLD_MEDIUM;
        }
        else {
            threshold = TODO_SIMILARITY_THRESHOLD;
        }
        let bestMatch;
        let bestSimilarity = 0;
        for (const todo of this._todos.values()) {
            const similarity = this.calculateSimilarity(content, todo.content);
            if (similarity >= threshold && similarity > bestSimilarity) {
                bestSimilarity = similarity;
                bestMatch = todo;
            }
        }
        return bestMatch;
    }
    // ========== P1-009: Progress Estimation Methods ==========
    /**
     * Estimate complexity of a todo based on content keywords.
     */
    estimateComplexity(content) {
        const lower = content.toLowerCase();
        const trivialPatterns = [
            /\btypo\b/,
            /\bspelling\b/,
            /\bcomment\b/,
            /\bupdate\s+(?:version|readme)\b/,
            /\brename\b/,
            /\bformat(?:ting)?\b/,
        ];
        const complexPatterns = [
            /\barchitect(?:ure)?\b/,
            /\brefactor\b/,
            /\brewrite\b/,
            /\bsecurity\b/,
            /\bmigrat(?:e|ion)\b/,
            /\btest(?:s|ing)?\b/,
            /\bintegrat(?:e|ion)\b/,
            /\bperformance\b/,
            /\boptimiz(?:e|ation)\b/,
            /\bmultiple\s+files?\b/,
        ];
        const moderatePatterns = [/\bbug\b/, /\bfeature\b/, /\benhance(?:ment)?\b/, /\bimplement\b/, /\badd\b/, /\bfix\b/];
        for (const pattern of complexPatterns) {
            if (pattern.test(lower))
                return 'complex';
        }
        for (const trivialPattern of trivialPatterns) {
            if (trivialPattern.test(lower))
                return 'trivial';
        }
        for (const moderatePattern of moderatePatterns) {
            if (moderatePattern.test(lower))
                return 'moderate';
        }
        return 'simple';
    }
    /**
     * Get estimated duration for a complexity level (ms).
     */
    getEstimatedDuration(complexity) {
        const avgTime = this.getAverageCompletionTime();
        if (avgTime !== null) {
            const multipliers = {
                trivial: 0.25,
                simple: 0.5,
                moderate: 1.0,
                complex: 2.0,
            };
            return Math.round(avgTime * multipliers[complexity]);
        }
        const defaults = {
            trivial: 1 * 60 * 1000,
            simple: 3 * 60 * 1000,
            moderate: 10 * 60 * 1000,
            complex: 30 * 60 * 1000,
        };
        return defaults[complexity];
    }
    /**
     * Get average completion time from historical data.
     */
    getAverageCompletionTime() {
        if (this._completionTimes.length === 0)
            return null;
        const sum = this._completionTimes.reduce((a, b) => a + b, 0);
        return Math.round(sum / this._completionTimes.length);
    }
    /**
     * Record a todo completion for progress tracking.
     */
    recordTodoCompletion(todoId) {
        const startTime = this._todoStartTimes.get(todoId);
        if (startTime) {
            const duration = Date.now() - startTime;
            this._completionTimes.push(duration);
            while (this._completionTimes.length > RalphTracker.MAX_COMPLETION_TIMES) {
                this._completionTimes.shift();
            }
            this._todoStartTimes.delete(todoId);
        }
    }
    /**
     * Start tracking a todo for duration estimation.
     */
    startTrackingTodo(todoId) {
        if (!this._todoStartTimes.has(todoId)) {
            this._todoStartTimes.set(todoId, Date.now());
        }
        if (this._todosStartedAt === 0) {
            this._todosStartedAt = Date.now();
        }
    }
    /**
     * Get progress estimation for the todo list.
     */
    getTodoProgress() {
        const todos = Array.from(this._todos.values());
        const total = todos.length;
        const completed = todos.filter((t) => t.status === 'completed').length;
        const inProgress = todos.filter((t) => t.status === 'in_progress').length;
        const pending = todos.filter((t) => t.status === 'pending').length;
        const percentComplete = total > 0 ? Math.round((completed / total) * 100) : 0;
        let estimatedRemainingMs = null;
        let avgCompletionTimeMs = null;
        let projectedCompletionAt = null;
        avgCompletionTimeMs = this.getAverageCompletionTime();
        if (total > 0 && completed > 0) {
            if (avgCompletionTimeMs !== null) {
                const remaining = total - completed;
                estimatedRemainingMs = remaining * avgCompletionTimeMs;
            }
            else {
                const elapsed = Date.now() - this._todosStartedAt;
                if (elapsed > 0 && completed > 0) {
                    const timePerTodo = elapsed / completed;
                    avgCompletionTimeMs = Math.round(timePerTodo);
                    const remaining = total - completed;
                    estimatedRemainingMs = Math.round(remaining * timePerTodo);
                }
            }
            if (estimatedRemainingMs !== null) {
                projectedCompletionAt = Date.now() + estimatedRemainingMs;
            }
        }
        else if (total > 0 && completed === 0) {
            let totalEstimate = 0;
            for (const todo of todos) {
                if (todo.status !== 'completed') {
                    const complexity = todo.estimatedComplexity || this.estimateComplexity(todo.content);
                    totalEstimate += this.getEstimatedDuration(complexity);
                }
            }
            estimatedRemainingMs = totalEstimate;
            projectedCompletionAt = Date.now() + totalEstimate;
        }
        return {
            total,
            completed,
            inProgress,
            pending,
            percentComplete,
            estimatedRemainingMs,
            avgCompletionTimeMs,
            projectedCompletionAt,
        };
    }
    /**
     * Generate a stable ID from todo content using content hashing.
     */
    generateTodoId(content) {
        if (!content)
            return 'todo-empty';
        const hash = todoContentHash(content);
        return `todo-${hash}`;
    }
    /**
     * Find the todo item with the oldest detectedAt timestamp.
     */
    findOldestTodo() {
        let oldest;
        for (const todo of this._todos.values()) {
            if (!oldest || todo.detectedAt < oldest.detectedAt) {
                oldest = todo;
            }
        }
        return oldest;
    }
    /**
     * Conditionally run cleanup, throttled to CLEANUP_THROTTLE_MS.
     */
    maybeCleanupExpiredTodos() {
        const now = Date.now();
        if (now - this._lastCleanupTime < CLEANUP_THROTTLE_MS) {
            return;
        }
        this._lastCleanupTime = now;
        this.cleanupExpiredTodos();
    }
    /**
     * Remove todo items older than TODO_EXPIRY_MS.
     */
    cleanupExpiredTodos() {
        const now = Date.now();
        const toDelete = [];
        for (const [id, todo] of this._todos) {
            if (now - todo.detectedAt > TODO_EXPIRY_MS) {
                toDelete.push(id);
            }
        }
        if (toDelete.length > 0) {
            for (const id of toDelete) {
                this._todos.delete(id);
                this._todoStartTimes.delete(id);
            }
            this.emit('todoUpdate', this.todos);
        }
    }
    /**
     * Programmatically start a loop (external API).
     */
    startLoop(completionPhrase, maxIterations) {
        this.enable();
        this._loopState.active = true;
        this._loopState.startedAt = Date.now();
        this._loopState.cycleCount = 0;
        this._loopState.maxIterations = maxIterations ?? null;
        this._loopState.elapsedHours = null;
        this._loopState.lastActivity = Date.now();
        if (completionPhrase) {
            this._loopState.completionPhrase = completionPhrase;
        }
        this.emit('loopUpdate', this.loopState);
    }
    /**
     * Update the maximum iteration count (external API).
     */
    setMaxIterations(maxIterations) {
        this._loopState.maxIterations = maxIterations;
        this._loopState.lastActivity = Date.now();
        this.emit('loopUpdate', this.loopState);
    }
    /**
     * Configure the tracker from external state.
     */
    configure(config) {
        if (config.enabled !== undefined) {
            this._loopState.enabled = config.enabled;
        }
        if (config.completionPhrase !== undefined) {
            this._loopState.completionPhrase = config.completionPhrase;
        }
        if (config.maxIterations !== undefined) {
            this._loopState.maxIterations = config.maxIterations;
        }
        this._loopState.lastActivity = Date.now();
        this.emit('loopUpdate', this.loopState);
    }
    /**
     * Programmatically stop the loop (external API).
     */
    stopLoop() {
        this._loopState.active = false;
        this._loopState.lastActivity = Date.now();
        this.emit('loopUpdate', this.loopState);
    }
    /**
     * Enforce size limit on _taskNumberToContent map.
     */
    enforceTaskMappingLimit() {
        if (this._taskNumberToContent.size <= MAX_TASK_MAPPINGS)
            return;
        const sortedKeys = Array.from(this._taskNumberToContent.keys()).sort((a, b) => a - b);
        const keysToRemove = sortedKeys.slice(0, this._taskNumberToContent.size - MAX_TASK_MAPPINGS);
        for (const key of keysToRemove) {
            this._taskNumberToContent.delete(key);
        }
    }
    /**
     * Clear all state and disable the tracker.
     */
    clear() {
        this.clearDebounceTimers();
        this.fixPlanWatcher.stop();
        this.stallDetector.stopIterationStallDetection();
        this._loopState = createInitialRalphTrackerState();
        this._todos.clear();
        this._taskNumberToContent.clear();
        this._todoStartTimes.clear();
        this._alternateCompletionPhrases.clear();
        this._lineBuffer = '';
        this._partialPromiseBuffer = '';
        this._completionPhraseCount.clear();
        // Clear sub-module state
        this.statusParser.fullReset();
        this.planTracker.fullReset();
        this.stallDetector.reset();
        this.emit('loopUpdate', this.loopState);
        this.emit('todoUpdate', this.todos);
    }
    /**
     * Get aggregated statistics about tracked todos.
     */
    getTodoStats() {
        let pending = 0;
        let inProgress = 0;
        let completed = 0;
        for (const todo of this._todos.values()) {
            switch (todo.status) {
                case 'pending':
                    pending++;
                    break;
                case 'in_progress':
                    inProgress++;
                    break;
                case 'completed':
                    completed++;
                    break;
            }
        }
        return {
            total: this._todos.size,
            pending,
            inProgress,
            completed,
        };
    }
    /**
     * Restore tracker state from persisted data.
     */
    restoreState(loopState, todos) {
        this._loopState = {
            ...loopState,
            enabled: loopState.enabled ?? false,
        };
        this._todos.clear();
        for (const todo of todos) {
            this._todos.set(todo.id, {
                ...todo,
                priority: todo.priority ?? null,
            });
        }
    }
    /**
     * Clean up all resources and release memory.
     */
    destroy() {
        this.cleanup.dispose();
        this._todoDeb.dispose();
        this._loopDeb.dispose();
        this.fixPlanWatcher.destroy();
        this.stallDetector.destroy();
        this.statusParser.destroy();
        this.planTracker.destroy();
        this._todos.clear();
        this._taskNumberToContent.clear();
        this._todoStartTimes.clear();
        this._alternateCompletionPhrases.clear();
        this._completionPhraseCount.clear();
        this._completionTimes.length = 0;
        this._lineBuffer = '';
        this._partialPromiseBuffer = '';
        this.removeAllListeners();
    }
}
//# sourceMappingURL=ralph-tracker.js.map