Files
Codeman/src/ralph-plan-tracker.ts
T
arkonandClaude Opus 4.6 e7a9cbe442 refactor: split god files into focused modules (phase 4, steps 2-4)
Split 3 large files into 11 focused sub-modules via composition:

ralph-tracker.ts (3,868 → ~2,400 LOC):
- ralph-plan-tracker.ts: plan task tracking, checkpoints, history
- ralph-fix-plan-watcher.ts: @fix_plan.md file watching
- ralph-stall-detector.ts: iteration stall detection
- ralph-status-parser.ts: RALPH_STATUS block parsing, circuit breaker

respawn-controller.ts (3,611 → ~3,200 LOC):
- respawn-patterns.ts: pure pattern detection functions
- respawn-adaptive-timing.ts: adaptive timing with percentile calc
- respawn-metrics.ts: cycle metrics tracking + aggregation
- respawn-health.ts: pure health scoring functions

session.ts (2,418 → ~1,800 LOC):
- session-cli-builder.ts: CLI argument construction
- session-auto-ops.ts: auto-compact/clear automation
- session-task-cache.ts: task description LRU cache

All external APIs preserved via delegation. Events forwarded
through parent classes. Zero behavioral changes — all 436 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 04:04:35 +01:00

478 lines
13 KiB
TypeScript

/**
* @fileoverview RalphPlanTracker - Enhanced plan task management
*
* Manages plan tasks with verification criteria, dependencies,
* execution tracking, TDD workflow support, and plan versioning.
*
* Extracted from ralph-tracker.ts as part of domain splitting.
*
* @module ralph-plan-tracker
*/
import { EventEmitter } from 'node:events';
import type { PlanTaskStatus, TddPhase } from './types.js';
// ========== Enhanced Plan Task Interface ==========
/**
* Enhanced plan task with verification criteria, dependencies, and execution tracking.
* Supports TDD workflow, failure tracking, and plan versioning.
*/
export interface EnhancedPlanTask {
/** Unique identifier (e.g., "P0-001") */
id: string;
/** Task description */
content: string;
/** Criticality level */
priority: 'P0' | 'P1' | 'P2' | null;
/** How to verify completion */
verificationCriteria?: string;
/** Command to run for verification */
testCommand?: string;
/** IDs of tasks that must complete first */
dependencies: string[];
/** Current execution status */
status: PlanTaskStatus;
/** How many times attempted */
attempts: number;
/** Most recent failure reason */
lastError?: string;
/** Timestamp of completion */
completedAt?: number;
/** Plan version this belongs to */
version: number;
/** TDD phase category */
tddPhase?: TddPhase;
/** ID of paired test/impl task */
pairedWith?: string;
/** Estimated complexity */
complexity?: 'low' | 'medium' | 'high';
/** Checklist items for review tasks (tddPhase: 'review') */
reviewChecklist?: string[];
}
/** Checkpoint review data */
export interface CheckpointReview {
iteration: number;
timestamp: number;
summary: {
total: number;
completed: number;
failed: number;
blocked: number;
pending: number;
inProgress: number;
};
stuckTasks: Array<{
id: string;
content: string;
attempts: number;
lastError?: string;
}>;
recommendations: string[];
}
const MAX_PLAN_HISTORY = 10;
/**
* RalphPlanTracker - Manages enhanced plan tasks with versioning and checkpoints.
*
* Events emitted:
* - `planInitialized` - When a new plan is initialized
* - `planTaskUpdate` - When a plan task is updated
* - `taskBlocked` - When a task becomes blocked after too many failures
* - `taskUnblocked` - When a task's dependencies are all met
* - `planCheckpoint` - When a checkpoint review is triggered
* - `planTaskAdded` - When a new task is added to the plan
* - `planRollback` - When the plan is rolled back to a previous version
*/
export class RalphPlanTracker extends EventEmitter {
/** Current version of the plan (incremented on changes) */
private _planVersion: number = 1;
/** History of plan versions for rollback support */
private _planHistory: Array<{
version: number;
timestamp: number;
tasks: Map<string, EnhancedPlanTask>;
summary: string;
}> = [];
/** Enhanced plan tasks with execution tracking */
private _planTasks: Map<string, EnhancedPlanTask> = new Map();
/** Checkpoint intervals (iterations at which to trigger review) */
private _checkpointIterations: number[] = [5, 10, 20, 30, 50, 75, 100];
/** Last checkpoint iteration */
private _lastCheckpointIteration: number = 0;
/** Current cycle count (fed by parent via notifyCycleCount) */
private _cycleCount: number = 0;
constructor() {
super();
}
/**
* Notify the plan tracker of the current cycle count.
* Called by parent when iteration changes (for checkpoint detection).
*/
notifyCycleCount(cycleCount: number): void {
this._cycleCount = cycleCount;
}
/**
* 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 {
// Save current plan to history before replacing
if (this._planTasks.size > 0) {
this._savePlanToHistory('Plan replaced with new generation');
}
// Clear and rebuild
this._planTasks.clear();
this._planVersion++;
items.forEach((item, idx) => {
const id = item.id || `task-${idx}`;
const task: EnhancedPlanTask = {
id,
content: item.content,
priority: item.priority || null,
verificationCriteria: item.verificationCriteria,
testCommand: item.testCommand,
dependencies: item.dependencies || [],
status: 'pending',
attempts: 0,
version: this._planVersion,
tddPhase: item.tddPhase,
pairedWith: item.pairedWith,
complexity: item.complexity,
};
this._planTasks.set(id, task);
});
this.emit('planInitialized', { version: this._planVersion, taskCount: this._planTasks.size });
}
/**
* 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 } {
const task = this._planTasks.get(taskId);
if (!task) {
return { success: false, error: 'Task not found' };
}
if (update.status) {
task.status = update.status;
if (update.status === 'completed') {
task.completedAt = Date.now();
}
}
if (update.error) {
task.lastError = update.error;
}
if (update.incrementAttempts) {
task.attempts++;
// After 3 failed attempts, mark as blocked and emit warning
if (task.attempts >= 3 && task.status === 'failed') {
task.status = 'blocked';
this.emit('taskBlocked', {
taskId,
content: task.content,
attempts: task.attempts,
lastError: task.lastError,
});
}
}
// Update blocked tasks when a dependency completes
if (update.status === 'completed') {
this._unblockDependentTasks(taskId);
}
// Check for checkpoint
this._checkForCheckpoint();
this.emit('planTaskUpdate', { taskId, task });
return { success: true, task };
}
/**
* Add a new task to the plan (for runtime adaptation).
*/
addPlanTask(task: {
content: string;
priority?: 'P0' | 'P1' | 'P2';
verificationCriteria?: string;
dependencies?: string[];
insertAfter?: string;
}): { task: EnhancedPlanTask } {
// 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 id = `${prefix}-${String(counter).padStart(3, '0')}`;
while (this._planTasks.has(id)) {
counter++;
id = `${prefix}-${String(counter).padStart(3, '0')}`;
}
const newTask: EnhancedPlanTask = {
id,
content: task.content,
priority: task.priority || null,
verificationCriteria: task.verificationCriteria || 'Task completed successfully',
dependencies: task.dependencies || [],
status: 'pending',
attempts: 0,
version: this._planVersion,
};
this._planTasks.set(id, newTask);
this.emit('planTaskAdded', { task: newTask });
return { task: newTask };
}
/**
* Get all plan tasks.
*/
getPlanTasks(): EnhancedPlanTask[] {
return Array.from(this._planTasks.values());
}
/**
* Generate a checkpoint review summarizing plan progress and stuck tasks.
*/
generateCheckpointReview(): CheckpointReview {
const tasks = Array.from(this._planTasks.values());
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,
};
// Find stuck tasks (3+ attempts or blocked)
const stuckTasks = tasks
.filter((t) => t.attempts >= 3 || t.status === 'blocked')
.map((t) => ({
id: t.id,
content: t.content,
attempts: t.attempts,
lastError: t.lastError,
}));
// Generate recommendations
const recommendations: string[] = [];
if (stuckTasks.length > 0) {
recommendations.push(`${stuckTasks.length} task(s) are stuck. Consider breaking them into smaller steps.`);
}
if (summary.failed > summary.completed && summary.total > 5) {
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;
if (progressPercent < 20 && this._cycleCount > 10) {
recommendations.push('Progress is slow. Consider simplifying tasks or reviewing dependencies.');
}
if (summary.total > 0 && summary.blocked > summary.total / 3) {
recommendations.push('Many tasks are blocked. Review dependency chain for bottlenecks.');
}
return {
iteration: this._cycleCount,
timestamp: Date.now(),
summary,
stuckTasks,
recommendations,
};
}
/**
* Get plan version history.
*/
getPlanHistory(): Array<{
version: number;
timestamp: number;
summary: string;
stats: { total: number; completed: number; failed: number };
}> {
return this._planHistory.map((h) => {
const tasks = Array.from(h.tasks.values());
return {
version: h.version,
timestamp: h.timestamp,
summary: h.summary,
stats: {
total: tasks.length,
completed: tasks.filter((t) => t.status === 'completed').length,
failed: tasks.filter((t) => t.status === 'failed').length,
},
};
});
}
/**
* Rollback to a previous plan 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` };
}
// Save current state first
this._savePlanToHistory(`Rolled back from v${this._planVersion} to v${version}`);
// Restore the historical version
this._planTasks.clear();
for (const [id, task] of historyEntry.tasks) {
// Reset execution state for retry
this._planTasks.set(id, {
...task,
status: task.status === 'completed' ? 'completed' : 'pending',
attempts: task.status === 'completed' ? task.attempts : 0,
lastError: undefined,
});
}
this._planVersion++;
this.emit('planRollback', { version, newVersion: this._planVersion });
return { success: true, plan: Array.from(this._planTasks.values()) };
}
/**
* Check if checkpoint review is due for current iteration.
*/
isCheckpointDue(): boolean {
return this._checkpointIterations.includes(this._cycleCount) && this._cycleCount > this._lastCheckpointIteration;
}
/**
* Get current plan version.
*/
get planVersion(): number {
return this._planVersion;
}
/**
* Reset plan state (soft reset - keeps version history).
*/
reset(): void {
// Don't clear history or version on soft reset
}
/**
* Full reset - clears all plan state.
*/
fullReset(): void {
this._planTasks.clear();
this._planHistory.length = 0;
this._planVersion = 1;
this._lastCheckpointIteration = 0;
this._cycleCount = 0;
}
/**
* Clean up all resources.
*/
destroy(): void {
this._planTasks.clear();
this._planHistory.length = 0;
this.removeAllListeners();
}
/**
* Unblock tasks that were waiting on a completed dependency.
*/
private _unblockDependentTasks(completedTaskId: string): void {
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 dep = this._planTasks.get(depId);
return dep && dep.status === 'completed';
});
if (allDepsComplete && task.status === 'blocked') {
task.status = 'pending';
this.emit('taskUnblocked', { taskId: task.id });
}
}
}
}
/**
* Check if current iteration is a checkpoint and emit review if so.
*/
private _checkForCheckpoint(): void {
if (this._checkpointIterations.includes(this._cycleCount) && this._cycleCount > this._lastCheckpointIteration) {
this._lastCheckpointIteration = this._cycleCount;
const checkpoint = this.generateCheckpointReview();
this.emit('planCheckpoint', checkpoint);
}
}
/**
* Save current plan state to history.
*/
private _savePlanToHistory(summary: string): void {
// Clone current tasks
const tasksCopy = new Map<string, EnhancedPlanTask>();
for (const [id, task] of this._planTasks) {
tasksCopy.set(id, { ...task });
}
this._planHistory.push({
version: this._planVersion,
timestamp: Date.now(),
tasks: tasksCopy,
summary,
});
// Limit history size
if (this._planHistory.length > MAX_PLAN_HISTORY) {
this._planHistory.shift();
}
}
}