From bf44925ea4a70185cd44309bd3694f962c09d46e Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 22 Jan 2026 22:02:49 +0100 Subject: [PATCH] feat: detect Claude Code checkmark-based TodoWrite output format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RalphTracker now recognizes Format 5 - the checkmark-based output that Claude Code's TodoWrite tool produces: - "✔ Task #N created: " → creates todo - "✔ #N " → registers task summary - "✔ Task #N updated: status → in progress/completed" → updates status Previously only detected markdown checkboxes, icon indicators, and native ☐/☒/◐ format, missing the primary output format used by Claude Code CLI sessions. Co-Authored-By: Claude Opus 4.5 --- src/ralph-tracker.ts | 77 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts index d0b0f7c8..8273d254 100644 --- a/src/ralph-tracker.ts +++ b/src/ralph-tracker.ts @@ -112,6 +112,18 @@ const TODO_STATUS_PATTERN = /[-*]\s*(.+?)\s+\((pending|in_progress|completed)\)/ */ 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; + /** * Patterns to exclude from todo detection * Prevents false positives from tool invocations and Claude commentary @@ -300,6 +312,9 @@ export class RalphTracker extends EventEmitter { /** When true, prevents auto-enable on pattern detection */ private _autoEnableDisabled: boolean = false; + /** Maps task numbers from "✔ Task #N" format to their content for status updates */ + private _taskNumberToContent: Map = new Map(); + /** * Creates a new RalphTracker instance. * Starts in disabled state until Ralph patterns are detected. @@ -394,6 +409,7 @@ export class RalphTracker extends EventEmitter { this._loopState.enabled = wasEnabled; // Keep enabled status this._todos.clear(); this._completionPhraseCount.clear(); + this._taskNumberToContent.clear(); this._lineBuffer = ''; // Emit immediately on reset (no debounce) this.emit('loopUpdate', this.loopState); @@ -414,6 +430,7 @@ export class RalphTracker extends EventEmitter { this._loopState = createInitialRalphTrackerState(); this._todos.clear(); this._completionPhraseCount.clear(); + this._taskNumberToContent.clear(); this._lineBuffer = ''; // Emit immediately on reset (no debounce) this.emit('loopUpdate', this.loopState); @@ -635,6 +652,16 @@ export class RalphTracker extends EventEmitter { return true; } + // Claude Code checkmark-based TodoWrite: "✔ Task #N created:", "✔ Task #N updated:" + 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; + } + // Loop start patterns (e.g., "Loop started at", "Starting Ralph loop") if (LOOP_START_PATTERN.test(data)) { return true; @@ -1056,9 +1083,10 @@ export class RalphTracker extends EventEmitter { const hasTodoIndicator = line.includes('Todo:'); const hasNativeCheckbox = line.includes('☐') || line.includes('☒') || line.includes('◐'); const hasStatus = line.includes('(pending)') || line.includes('(in_progress)') || line.includes('(completed)'); + const hasCheckmark = line.includes('✔'); // Quick check: skip lines that can't possibly contain todos - if (!hasCheckbox && !hasTodoIndicator && !hasNativeCheckbox && !hasStatus) { + if (!hasCheckbox && !hasTodoIndicator && !hasNativeCheckbox && !hasStatus && !hasCheckmark) { return; } @@ -1124,6 +1152,52 @@ export class RalphTracker extends EventEmitter { } } + // Format 5: Claude Code checkmark-based TodoWrite output (✔ Task #N) + // Handles: "✔ Task #N created: content", "✔ #N content", "✔ Task #N updated: status → X" + if (hasCheckmark) { + // Task creation: "✔ Task #1 created: Fix the bug" + TODO_TASK_CREATED_PATTERN.lastIndex = 0; + while ((match = TODO_TASK_CREATED_PATTERN.exec(line)) !== null) { + const taskNum = parseInt(match[1], 10); + const content = match[2].trim(); + if (content.length >= 5) { + this._taskNumberToContent.set(taskNum, content); + this.upsertTodo(content, 'pending'); + updated = true; + } + } + + // Task summary: "✔ #1 Fix the bug" + TODO_TASK_SUMMARY_PATTERN.lastIndex = 0; + while ((match = TODO_TASK_SUMMARY_PATTERN.exec(line)) !== null) { + const taskNum = parseInt(match[1], 10); + const content = match[2].trim(); + if (content.length >= 5) { + // Only register if not already known from a "created" line + if (!this._taskNumberToContent.has(taskNum)) { + this._taskNumberToContent.set(taskNum, content); + } + this.upsertTodo(this._taskNumberToContent.get(taskNum) || content, 'pending'); + updated = true; + } + } + + // Status update: "✔ Task #1 updated: status → completed" + TODO_TASK_STATUS_PATTERN.lastIndex = 0; + while ((match = TODO_TASK_STATUS_PATTERN.exec(line)) !== null) { + const taskNum = parseInt(match[1], 10); + const statusStr = match[2].trim(); + const status: RalphTodoStatus = 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) { // Use debounced emit to batch rapid todo updates and reduce UI jitter this.emitTodoUpdateDebounced(); @@ -1378,6 +1452,7 @@ export class RalphTracker extends EventEmitter { this.clearDebounceTimers(); this._loopState = createInitialRalphTrackerState(); // This sets enabled: false this._todos.clear(); + this._taskNumberToContent.clear(); this._lineBuffer = ''; this._completionPhraseCount.clear(); this.emit('loopUpdate', this.loopState);