chore: project setup and tooling cleanup (#26)

* add project setup

* add tools to eslint

* remove contributing.md

* update claude md

* chore: simplify CI to single Node.js version (22)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* use node 20

* adjust formatting

* fix linting

* format

* fix layout

* fix: align CI node version to .nvmrc (22), remove redundant gotcha

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: arkon <arkon.85@hotmail.com>
This commit is contained in:
Noah Waldner
2026-02-27 13:16:23 +01:00
committed by GitHub
co-authored by Claude Sonnet 4.6 arkon
parent 2b2829ebde
commit 480584de63
61 changed files with 4306 additions and 4013 deletions
+179 -154
View File
@@ -34,12 +34,7 @@ import {
PlanTaskStatus,
TddPhase,
} from './types.js';
import {
ANSI_ESCAPE_PATTERN_SIMPLE,
fuzzyPhraseMatch,
todoContentHash,
stringSimilarity,
} from './utils/index.js';
import { ANSI_ESCAPE_PATTERN_SIMPLE, fuzzyPhraseMatch, todoContentHash, stringSimilarity } from './utils/index.js';
import { MAX_LINE_BUFFER_SIZE } from './config/buffer-limits.js';
import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
@@ -151,8 +146,19 @@ const MAX_PLAN_HISTORY = 10;
* 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',
'DONE',
'COMPLETE',
'FINISHED',
'OK',
'YES',
'TRUE',
'SUCCESS',
'READY',
'COMPLETED',
'PASSED',
'END',
'STOP',
'EXIT',
]);
/**
@@ -249,9 +255,9 @@ const TODO_PLAIN_CHECKMARK_PATTERN = /✔\s+(.+)/g;
* 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
/^(?: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 ----------
@@ -313,7 +319,8 @@ const TODOWRITE_PATTERN = /TodoWrite|todo(?:s)?\s*(?:updated|written|saved)|Todo
* 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;
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
@@ -327,7 +334,8 @@ const ALL_COUNT_PATTERN = /all\s+(\d+)\s+(?:tasks?|files?|items?)/i;
* 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;
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 ----------
@@ -411,48 +419,48 @@ const COMPLETION_INDICATOR_PATTERNS = [
/** 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
/\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
/\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
/\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 ==========
@@ -950,7 +958,7 @@ export class RalphTracker extends EventEmitter {
const wasEnabled = this._loopState.enabled;
this._loopState = createInitialRalphTrackerState();
this._loopState.enabled = wasEnabled; // Keep enabled status
this._loopState.enabled = wasEnabled; // Keep enabled status
this._todos.clear();
this._completionPhraseCount.clear();
this._taskNumberToContent.clear();
@@ -1241,7 +1249,7 @@ export class RalphTracker extends EventEmitter {
// 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')) {
if (todoArray.length > 0 && todoArray.every((t) => t.status === 'completed')) {
signals.allTodosComplete = true;
score += 20;
}
@@ -1262,10 +1270,12 @@ export class RalphTracker extends EventEmitter {
if (context) {
const lowerContext = context.toLowerCase();
// Deduct points if phrase appears in prompt-like context
if (lowerContext.includes('output:') ||
lowerContext.includes('completion phrase') ||
lowerContext.includes('output exactly') ||
lowerContext.includes('when done')) {
if (
lowerContext.includes('output:') ||
lowerContext.includes('completion phrase') ||
lowerContext.includes('output exactly') ||
lowerContext.includes('when done')
) {
signals.contextAppropriate = false;
score -= 20;
} else {
@@ -1335,7 +1345,6 @@ export class RalphTracker extends EventEmitter {
* Use this when the caller has already stripped ANSI to avoid redundant regex work.
*/
processCleanData(cleanData: string): void {
// If tracker is disabled, only check for patterns that should auto-enable it
if (!this._loopState.enabled) {
// Don't auto-enable if explicitly disabled by user setting
@@ -1396,14 +1405,21 @@ export class RalphTracker extends EventEmitter {
// substrings that any pattern could match are present in the data.
// This avoids 12 regex tests on every PTY chunk (the common case).
if (
!data.includes('<') && // <promise>, TodoWrite
!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('<') && // <promise>, TodoWrite
!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')
!data.includes('Loop') &&
!data.includes('complete') &&
!data.includes('COMPLETE') &&
!data.includes('Done') &&
!data.includes('DONE')
) {
return false;
}
@@ -1677,8 +1693,8 @@ export class RalphTracker extends EventEmitter {
// Avoid false positives: don't trigger on prompt context
const isNotInPromptContext = !line.includes('<promise>') && !line.includes('output:');
// Also avoid triggering on "completion phrase is X" explanatory text
const isNotExplanation = !line.toLowerCase().includes('completion phrase') &&
!line.toLowerCase().includes('output exactly');
const isNotExplanation =
!line.toLowerCase().includes('completion phrase') && !line.toLowerCase().includes('output exactly');
if (isNotInPromptContext && isNotExplanation) {
this.handleBareCompletionPhrase(expectedPhrase);
@@ -1786,7 +1802,7 @@ export class RalphTracker extends EventEmitter {
if (matchedPhrase) {
// Use the matched phrase (canonical) for tracking
const canonicalCount = (this._completionPhraseCount.get(matchedPhrase) || 0);
const canonicalCount = this._completionPhraseCount.get(matchedPhrase) || 0;
// Require 2nd+ occurrence of canonical phrase OR explicitly active loop.
// First occurrence (count=1) is the prompt echo — not actual completion.
if (canonicalCount >= 2 || this._loopState.active) {
@@ -1862,7 +1878,7 @@ export class RalphTracker extends EventEmitter {
* @fires phraseValidationWarning - When a risky phrase is detected
*/
private validateCompletionPhrase(phrase: string): void {
const normalized = phrase.toUpperCase().replace(/[\s_\-\.]+/g, '');
const normalized = phrase.toUpperCase().replace(/[\s_\-.]+/g, '');
// Generate a suggested unique phrase
const uniqueSuffix = Date.now().toString(36).slice(-4).toUpperCase();
@@ -1870,7 +1886,9 @@ export class RalphTracker extends EventEmitter {
// Check for common phrases
if (COMMON_COMPLETION_PHRASES.has(normalized)) {
console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is very common and may cause false positives. Consider using: "${suggestedPhrase}"`);
console.warn(
`[RalphTracker] Warning: Completion phrase "${phrase}" is very common and may cause false positives. Consider using: "${suggestedPhrase}"`
);
this.emit('phraseValidationWarning', {
phrase,
reason: 'common',
@@ -1881,7 +1899,9 @@ export class RalphTracker extends EventEmitter {
// Check for short phrases
if (normalized.length < MIN_RECOMMENDED_PHRASE_LENGTH) {
console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is too short (${normalized.length} chars). Consider using: "${suggestedPhrase}"`);
console.warn(
`[RalphTracker] Warning: Completion phrase "${phrase}" is too short (${normalized.length} chars). Consider using: "${suggestedPhrase}"`
);
this.emit('phraseValidationWarning', {
phrase,
reason: 'short',
@@ -1892,7 +1912,9 @@ export class RalphTracker extends EventEmitter {
// Check for numeric-only phrases
if (/^\d+$/.test(normalized)) {
console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is numeric-only and may cause false positives. Consider using: "${suggestedPhrase}"`);
console.warn(
`[RalphTracker] Warning: Completion phrase "${phrase}" is numeric-only and may cause false positives. Consider using: "${suggestedPhrase}"`
);
this.emit('phraseValidationWarning', {
phrase,
reason: 'numeric',
@@ -1971,14 +1993,16 @@ export class RalphTracker extends EventEmitter {
if (currentIter !== this._lastObservedIteration) {
this._lastIterationChangeTime = Date.now();
this._lastObservedIteration = currentIter;
this._iterationStallWarned = false; // Reset warning on iteration change
this._iterationStallWarned = false; // Reset warning on iteration change
// P1-004: Reset circuit breaker on successful iteration progress
// If we're making progress, the loop is healthy
if (this._circuitBreaker.state === 'HALF_OPEN' ||
this._circuitBreaker.consecutiveNoProgress > 0 ||
this._circuitBreaker.consecutiveSameError > 0 ||
this._circuitBreaker.consecutiveTestsFailure > 0) {
if (
this._circuitBreaker.state === 'HALF_OPEN' ||
this._circuitBreaker.consecutiveNoProgress > 0 ||
this._circuitBreaker.consecutiveSameError > 0 ||
this._circuitBreaker.consecutiveTestsFailure > 0
) {
this._circuitBreaker.consecutiveNoProgress = 0;
this._circuitBreaker.consecutiveSameError = 0;
this._circuitBreaker.lastProgressIteration = currentIter;
@@ -2106,7 +2130,7 @@ export class RalphTracker extends EventEmitter {
const content = match[2].trim();
// Skip if content matches exclude patterns (tool invocations, commentary)
const shouldExclude = TODO_EXCLUDE_PATTERNS.some(pattern => pattern.test(content));
const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
if (shouldExclude) continue;
// Skip if content is too short or looks like partial garbage
@@ -2155,9 +2179,8 @@ export class RalphTracker extends EventEmitter {
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 status: RalphTodoStatus =
statusStr === 'completed' ? 'completed' : statusStr === 'in progress' ? 'in_progress' : 'pending';
const content = this._taskNumberToContent.get(taskNum);
if (content) {
this.upsertTodo(content, status);
@@ -2172,7 +2195,7 @@ export class RalphTracker extends EventEmitter {
while ((match = TODO_PLAIN_CHECKMARK_PATTERN.exec(line)) !== null) {
const content = match[1].trim();
// Skip if content matches exclude patterns
const shouldExclude = TODO_EXCLUDE_PATTERNS.some(pattern => pattern.test(content));
const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
if (shouldExclude) continue;
if (content.length < 5) continue;
// Skip status/created/updated prefixed content (already handled above)
@@ -2204,17 +2227,17 @@ export class RalphTracker extends EventEmitter {
switch (icon) {
case '✓':
case '✅':
case '☒': // Claude Code checked checkbox
case '◉': // Filled circle (completed)
case '●': // Solid circle (completed)
case '☒': // Claude Code checked checkbox
case '◉': // Filled circle (completed)
case '●': // Solid circle (completed)
return 'completed';
case '◐': // Half-filled circle (in progress)
case '◐': // Half-filled circle (in progress)
case '⏳':
case '⌛':
case '🔄':
return 'in_progress';
case '☐': // Claude Code empty checkbox
case '○': // Empty circle
case '☐': // Claude Code empty checkbox
case '○': // Empty circle
default:
return 'pending';
}
@@ -2280,10 +2303,10 @@ export class RalphTracker extends EventEmitter {
// Clean content: remove ANSI codes, collapse whitespace, trim
const cleanContent = content
.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '') // Remove ANSI escape codes
.replace(/\s+/g, ' ') // Collapse whitespace
.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '') // Remove ANSI escape codes
.replace(/\s+/g, ' ') // Collapse whitespace
.trim();
if (cleanContent.length < 5) return; // Skip very short content
if (cleanContent.length < 5) return; // Skip very short content
// Parse priority from content
const priority = this.parsePriority(cleanContent);
@@ -2402,8 +2425,8 @@ export class RalphTracker extends EventEmitter {
private normalizeTodoContent(content: string): string {
if (!content) return '';
return content
.replace(/\s+/g, ' ') // Collapse whitespace
.replace(/[^a-zA-Z0-9\s.,!?'"-]/g, '') // Remove special chars (keep punctuation)
.replace(/\s+/g, ' ') // Collapse whitespace
.replace(/[^a-zA-Z0-9\s.,!?'"-]/g, '') // Remove special chars (keep punctuation)
.trim()
.toLowerCase();
}
@@ -2507,7 +2530,7 @@ export class RalphTracker extends EventEmitter {
if (normalized.length < 30) {
threshold = 0.95; // Very strict for short strings
} else if (normalized.length < 60) {
threshold = 0.90; // Strict for medium strings
threshold = 0.9; // Strict for medium strings
} else {
threshold = TODO_SIMILARITY_THRESHOLD; // 0.85 for longer strings
}
@@ -2563,14 +2586,7 @@ export class RalphTracker extends EventEmitter {
];
// Moderate: Bugs, features, enhancements
const moderatePatterns = [
/\bbug\b/,
/\bfeature\b/,
/\benhance(?:ment)?\b/,
/\bimplement\b/,
/\badd\b/,
/\bfix\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';
@@ -2609,10 +2625,10 @@ export class RalphTracker extends EventEmitter {
// Default estimates (in ms) based on typical task durations
const defaults = {
trivial: 1 * 60 * 1000, // 1 minute
simple: 3 * 60 * 1000, // 3 minutes
moderate: 10 * 60 * 1000, // 10 minutes
complex: 30 * 60 * 1000, // 30 minutes
trivial: 1 * 60 * 1000, // 1 minute
simple: 3 * 60 * 1000, // 3 minutes
moderate: 10 * 60 * 1000, // 10 minutes
complex: 30 * 60 * 1000, // 30 minutes
};
return defaults[complexity];
}
@@ -2671,9 +2687,9 @@ export class RalphTracker extends EventEmitter {
public getTodoProgress(): RalphTodoProgress {
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 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;
@@ -2983,7 +2999,7 @@ export class RalphTracker extends EventEmitter {
// Ensure enabled flag exists (backwards compatibility)
this._loopState = {
...loopState,
enabled: loopState.enabled ?? false, // Override after spread for backwards compat
enabled: loopState.enabled ?? false, // Override after spread for backwards compat
};
this._todos.clear();
for (const todo of todos) {
@@ -3068,7 +3084,9 @@ export class RalphTracker extends EventEmitter {
if (!Number.isNaN(value) && value >= 0) {
block.tasksCompletedThisLoop = value;
} else {
parseErrors.push(`Invalid TASKS_COMPLETED_THIS_LOOP value: "${tasksMatch[1]}". Expected: non-negative integer`);
parseErrors.push(
`Invalid TASKS_COMPLETED_THIS_LOOP value: "${tasksMatch[1]}". Expected: non-negative integer`
);
}
matched = true;
}
@@ -3104,7 +3122,9 @@ export class RalphTracker extends EventEmitter {
if (['IMPLEMENTATION', 'TESTING', 'DOCUMENTATION', 'REFACTORING'].includes(value)) {
block.workType = value as RalphWorkType;
} else {
parseErrors.push(`Invalid WORK_TYPE value: "${value}". Expected: IMPLEMENTATION, TESTING, DOCUMENTATION, or REFACTORING`);
parseErrors.push(
`Invalid WORK_TYPE value: "${value}". Expected: IMPLEMENTATION, TESTING, DOCUMENTATION, or REFACTORING`
);
}
matched = true;
}
@@ -3126,7 +3146,7 @@ export class RalphTracker extends EventEmitter {
// Track unknown fields for debugging (only if looks like a field)
if (!matched && trimmedLine.includes(':')) {
const fieldName = trimmedLine.split(':')[0].trim().toUpperCase();
if (fieldName && !['#', '//'].some(c => fieldName.startsWith(c))) {
if (fieldName && !['#', '//'].some((c) => fieldName.startsWith(c))) {
unknownFields.push(fieldName);
}
}
@@ -3221,11 +3241,7 @@ export class RalphTracker extends EventEmitter {
* @param status - Overall status from RALPH_STATUS
* @fires circuitBreakerUpdate - If state changes
*/
private updateCircuitBreaker(
hasProgress: boolean,
testsStatus: RalphTestsStatus,
status: RalphStatusValue
): void {
private updateCircuitBreaker(hasProgress: boolean, testsStatus: RalphTestsStatus, status: RalphStatusValue): void {
const prevState = this._circuitBreaker.state;
if (hasProgress) {
@@ -3319,7 +3335,11 @@ export class RalphTracker extends EventEmitter {
/**
* Get cumulative stats from status blocks.
*/
get cumulativeStats(): { filesModified: number; tasksCompleted: number; completionIndicators: number } {
get cumulativeStats(): {
filesModified: number;
tasksCompleted: number;
completionIndicators: number;
} {
return {
filesModified: this._totalFilesModified,
tasksCompleted: this._totalTasksCompleted,
@@ -3456,7 +3476,7 @@ export class RalphTracker extends EventEmitter {
const tasksHeaderPattern = /^##\s*Tasks/i;
// Pattern for todo items
const todoPattern = /^-\s*\[([ x\-])\]\s*(.+)$/;
const todoPattern = /^-\s*\[([ x-])\]\s*(.+)$/;
let inCompletedSection = false;
@@ -3504,7 +3524,7 @@ export class RalphTracker extends EventEmitter {
}
// Parse priority from content if not in a priority section
const parsedPriority = inCompletedSection ? null : (currentPriority || this.parsePriority(content));
const parsedPriority = inCompletedSection ? null : currentPriority || this.parsePriority(content);
const id = this.generateTodoId(content);
newTodos.push({
@@ -3535,17 +3555,19 @@ export class RalphTracker extends EventEmitter {
* Initialize plan tasks from generated plan items.
* Called when wizard generates a new plan.
*/
initializePlanTasks(items: Array<{
id?: string;
content: string;
priority?: 'P0' | 'P1' | 'P2' | null;
verificationCriteria?: string;
testCommand?: string;
dependencies?: string[];
tddPhase?: TddPhase;
pairedWith?: string;
complexity?: 'low' | 'medium' | 'high';
}>): void {
initializePlanTasks(
items: Array<{
id?: string;
content: string;
priority?: 'P0' | 'P1' | 'P2' | null;
verificationCriteria?: string;
testCommand?: string;
dependencies?: string[];
tddPhase?: TddPhase;
pairedWith?: string;
complexity?: 'low' | 'medium' | 'high';
}>
): void {
// Save current plan to history before replacing
if (this._planTasks.size > 0) {
this._savePlanToHistory('Plan replaced with new generation');
@@ -3580,11 +3602,14 @@ export class RalphTracker extends EventEmitter {
/**
* Update a specific plan task's status, attempts, or error.
*/
updatePlanTask(taskId: string, update: {
status?: PlanTaskStatus;
error?: string;
incrementAttempts?: boolean;
}): { success: boolean; task?: EnhancedPlanTask; error?: string } {
updatePlanTask(
taskId: string,
update: {
status?: PlanTaskStatus;
error?: string;
incrementAttempts?: boolean;
}
): { success: boolean; task?: EnhancedPlanTask; error?: string } {
const task = this._planTasks.get(taskId);
if (!task) {
return { success: false, error: 'Task not found' };
@@ -3635,7 +3660,7 @@ export class RalphTracker extends EventEmitter {
for (const [_, task] of this._planTasks) {
if (task.dependencies.includes(completedTaskId)) {
// Check if all dependencies are now complete
const allDepsComplete = task.dependencies.every(depId => {
const allDepsComplete = task.dependencies.every((depId) => {
const dep = this._planTasks.get(depId);
return dep && dep.status === 'completed';
});
@@ -3653,8 +3678,7 @@ export class RalphTracker extends EventEmitter {
*/
private _checkForCheckpoint(): void {
const currentIteration = this._loopState.cycleCount;
if (this._checkpointIterations.includes(currentIteration) &&
currentIteration > this._lastCheckpointIteration) {
if (this._checkpointIterations.includes(currentIteration) && currentIteration > this._lastCheckpointIteration) {
this._lastCheckpointIteration = currentIteration;
const checkpoint = this.generateCheckpointReview();
this.emit('planCheckpoint', checkpoint);
@@ -3669,17 +3693,17 @@ export class RalphTracker extends EventEmitter {
const summary = {
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length,
blocked: tasks.filter(t => t.status === 'blocked').length,
pending: tasks.filter(t => t.status === 'pending').length,
inProgress: tasks.filter(t => t.status === 'in_progress').length,
completed: tasks.filter((t) => t.status === 'completed').length,
failed: tasks.filter((t) => t.status === 'failed').length,
blocked: tasks.filter((t) => t.status === 'blocked').length,
pending: tasks.filter((t) => t.status === 'pending').length,
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
};
// Find stuck tasks (3+ attempts or blocked)
const stuckTasks = tasks
.filter(t => t.attempts >= 3 || t.status === 'blocked')
.map(t => ({
.filter((t) => t.attempts >= 3 || t.status === 'blocked')
.map((t) => ({
id: t.id,
content: t.content,
attempts: t.attempts,
@@ -3697,9 +3721,7 @@ export class RalphTracker extends EventEmitter {
recommendations.push('More tasks have failed than completed. Review approach and consider plan adjustment.');
}
const progressPercent = summary.total > 0
? Math.round((summary.completed / summary.total) * 100)
: 0;
const progressPercent = summary.total > 0 ? Math.round((summary.completed / summary.total) * 100) : 0;
if (progressPercent < 20 && this._loopState.cycleCount > 10) {
recommendations.push('Progress is slow. Consider simplifying tasks or reviewing dependencies.');
}
@@ -3749,7 +3771,7 @@ export class RalphTracker extends EventEmitter {
summary: string;
stats: { total: number; completed: number; failed: number };
}> {
return this._planHistory.map(h => {
return this._planHistory.map((h) => {
const tasks = Array.from(h.tasks.values());
return {
version: h.version,
@@ -3757,8 +3779,8 @@ export class RalphTracker extends EventEmitter {
summary: h.summary,
stats: {
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length,
completed: tasks.filter((t) => t.status === 'completed').length,
failed: tasks.filter((t) => t.status === 'failed').length,
},
};
});
@@ -3767,8 +3789,12 @@ export class RalphTracker extends EventEmitter {
/**
* Rollback to a previous plan version.
*/
rollbackToVersion(version: number): { success: boolean; plan?: EnhancedPlanTask[]; error?: string } {
const historyEntry = this._planHistory.find(h => h.version === version);
rollbackToVersion(version: number): {
success: boolean;
plan?: EnhancedPlanTask[];
error?: string;
} {
const historyEntry = this._planHistory.find((h) => h.version === version);
if (!historyEntry) {
return { success: false, error: `Version ${version} not found in history` };
}
@@ -3807,7 +3833,7 @@ export class RalphTracker extends EventEmitter {
// Generate unique ID
const existingIds = Array.from(this._planTasks.keys());
const prefix = task.priority || 'P1';
let counter = existingIds.filter(id => id.startsWith(prefix)).length + 1;
let counter = existingIds.filter((id) => id.startsWith(prefix)).length + 1;
let id = `${prefix}-${String(counter).padStart(3, '0')}`;
while (this._planTasks.has(id)) {
counter++;
@@ -3850,8 +3876,7 @@ export class RalphTracker extends EventEmitter {
*/
isCheckpointDue(): boolean {
const currentIteration = this._loopState.cycleCount;
return this._checkpointIterations.includes(currentIteration) &&
currentIteration > this._lastCheckpointIteration;
return this._checkpointIterations.includes(currentIteration) && currentIteration > this._lastCheckpointIteration;
}
/**