@papaoloba/nightly-code-orchestrator
Version:
Automated 8-hour coding sessions using Claude Code
1,510 lines (1,316 loc) • 92.1 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const { spawn } = require('cross-spawn');
const { EventEmitter } = require('events');
const winston = require('winston');
const pidusage = require('pidusage');
const YAML = require('yaml');
const { TaskManager } = require('./task-manager');
const { GitManager } = require('../integrations/git-manager');
const { Validator } = require('../utils/validator');
const { Reporter } = require('../utils/reporter');
const {
SuperClaudeIntegration
} = require('../integrations/superclaude-integration');
const {
SUPERCLAUDE_OPTIMIZATION_GUIDE
} = require('../integrations/superclaude-optimization-guide');
const { TIME, STORAGE, RETRY } = require('../utils/constants');
const PrettyLogger = require('../utils/pretty-logger');
const spinner = require('../utils/spinner');
/**
* Main orchestrator for nightly-claude-code automation
* Coordinates task execution, git operations, validation, and reporting
*
* @class Orchestrator
* @extends EventEmitter
*
* @fires Orchestrator#taskStarted - When a task begins execution
* @fires Orchestrator#taskCompleted - When a task completes successfully
* @fires Orchestrator#taskFailed - When a task fails
* @fires Orchestrator#sessionCompleted - When the entire session completes
*
* @example
* const orchestrator = new Orchestrator({
* configPath: 'nightly-code.yaml',
* tasksPath: 'nightly-tasks.yaml',
* maxDuration: 28800, // 8 hours
* dryRun: false
* });
*
* orchestrator.on('taskCompleted', (task) => {
* console.log(`Task ${task.id} completed`);
* });
*
* await orchestrator.run();
*/
class Orchestrator extends EventEmitter {
/**
* Create a new Orchestrator instance
*
* @param {Object} options - Configuration options
* @param {string} [options.configPath='nightly-code.yaml'] - Path to main configuration file
* @param {string} [options.tasksPath='nightly-tasks.yaml'] - Path to tasks configuration file
* @param {number} [options.maxDuration=28800] - Maximum session duration in seconds (default: 8 hours)
* @param {number} [options.checkpointInterval=300] - Checkpoint interval in seconds (default: 5 minutes)
* @param {boolean} [options.dryRun=false] - Run in dry-run mode without making changes
* @param {string|null} [options.resumeCheckpoint=null] - Checkpoint to resume from
* @param {string} [options.workingDir=process.cwd()] - Working directory for operations
* @param {boolean} [options.forceSuperclaude=false] - Force SuperClaude integration
* @param {number} [options.rateLimitRetries=5] - Number of retries for rate limiting
* @param {number} [options.rateLimitBaseDelay=60000] - Base delay for rate limiting in milliseconds
* @param {boolean} [options.enableRetryOnLimits=true] - Enable retry on rate limits
*/
constructor (options = {}) {
super();
this.options = {
configPath: options.configPath || 'nightly-code.yaml',
tasksPath: options.tasksPath || 'nightly-tasks.yaml',
maxDuration: options.maxDuration || TIME.SECONDS.MAX_SESSION_DURATION,
checkpointInterval:
options.checkpointInterval || TIME.SECONDS.DEFAULT_CHECKPOINT_INTERVAL,
dryRun: options.dryRun || false,
resumeCheckpoint: options.resumeCheckpoint || null,
workingDir: options.workingDir || process.cwd(),
forceSuperclaude: options.forceSuperclaude || false, // CLI flag to force SuperClaude
// Rate limiting and retry configuration
rateLimitRetries: options.rateLimitRetries || RETRY.DEFAULT_RETRIES,
rateLimitBaseDelay:
options.rateLimitBaseDelay || TIME.MS.RATE_LIMIT_BASE_DELAY,
enableRetryOnLimits: options.enableRetryOnLimits !== false, // Default to true
usageLimitRetry: options.usageLimitRetry !== false, // Default to true
rateLimitRetry: options.rateLimitRetry !== false, // Default to true
exponentialBackoff: options.exponentialBackoff !== false, // Default to true
jitter: options.jitter !== false, // Default to true
maxDelay: options.maxDelay || 18000000 // 5 hours for usage limits
};
this.state = {
startTime: null,
endTime: null,
currentTask: null,
completedTasks: [],
failedTasks: [],
checkpoints: [],
resourceUsage: [],
claudeProcess: null,
sessionId: this.generateSessionId()
};
// Initialize operation timers
this.operationTimers = new Map();
// Initialize pretty logger for enhanced UI
this.prettyLogger = new PrettyLogger();
// Set spinner to quiet mode when pretty logger is active
spinner.setQuietMode(true);
this.setupLogging();
this.setupComponents();
}
/**
* Generate a unique session ID based on current timestamp
* Format: session-YYYY-MM-DD-HHMMSS
*
* @returns {string} Unique session identifier
*/
generateSessionId () {
const date = new Date().toISOString().split('T')[0];
const time = new Date()
.toISOString()
.split('T')[1]
.split('.')[0]
.replace(/:/g, '');
return `session-${date}-${time}`;
}
// Helper methods for timing operations
startOperation (operationName) {
if (!this.operationTimers) {
this.operationTimers = new Map();
}
this.operationTimers.set(operationName, Date.now());
}
endOperation (operationName) {
if (!this.operationTimers || !this.operationTimers.has(operationName)) {
return '';
}
const startTime = this.operationTimers.get(operationName);
const duration = Date.now() - startTime;
this.operationTimers.delete(operationName);
const seconds = Math.round(duration / TIME.MS.ONE_SECOND);
const timeStr =
seconds >= 60
? `${Math.floor(seconds / 60)}m ${seconds % 60}s`
: `${seconds}s`;
return ` \x1b[35m[took ${timeStr}]\x1b[0m`; // Magenta color for operation timing
}
logWithTiming (level, message, operationName = null) {
const timing = operationName ? this.endOperation(operationName) : '';
this.logger[level](`${message}${timing}`);
}
// File-scoped logging methods
logSessionInfo (sessionInfo) {
this.logger.info(` \x1b[35m🧠 SuperClaude\x1b[0m │ ${sessionInfo}`);
}
logClaudeOutput (line) {
if (line.includes('Wave') || line.includes('wave')) {
this.logger.info(` \x1b[35m🤖 Claude\x1b[0m │ \x1b[35m${line}\x1b[0m`); // Magenta for waves
} else if (
line.includes('✅') ||
line.includes('Success') ||
line.includes('Completed')
) {
this.logger.info(` \x1b[32m🤖 Claude\x1b[0m │ \x1b[32m${line}\x1b[0m`); // Green for success
} else if (
line.includes('❌') ||
line.includes('Error') ||
line.includes('Failed')
) {
this.logger.info(` \x1b[31m🤖 Claude\x1b[0m │ \x1b[31m${line}\x1b[0m`); // Red for errors
} else if (line.includes('⚠️') || line.includes('Warning')) {
this.logger.info(` \x1b[33m🤖 Claude\x1b[0m │ \x1b[33m${line}\x1b[0m`); // Yellow for warnings
} else {
this.logger.info(` \x1b[36m🤖 Claude\x1b[0m │ ${line}`); // Cyan for robot icon, normal text
}
}
logClaudeError (line) {
this.logger.warn(`⚠️ Claude: ${line}`);
}
logValidationStatus (status, message) {
this.logger.info(`${status} ${message}`);
}
logTaskProgress (taskNum, totalTasks, message) {
this.logger.info(`📋 Task ${taskNum}/${totalTasks}: ${message}`);
}
logTaskStatus (label, value) {
this.displayInfo(`${label}: ${value}`);
}
logOperationStatus (icon, message) {
this.logger.info(`${icon} ${message}`);
}
logDebug (message, data) {
this.logger.debug(message, data);
}
logWarn (message, data = {}) {
this.logger.warn(message, data);
}
logError (message, data = {}) {
this.logger.error(message, data);
}
logInfo (message, data = {}) {
this.logger.info(message, data);
}
logSuperclaude (mode, message) {
this.logger.info(
` \x1b[${mode === 'framework' ? '35' : '36'}m🧠 ${
mode === 'framework' ? 'SuperClaude Framework' : 'Standard mode'
}\x1b[0m │ ${message}`
);
}
logPromptOptimization (message) {
this.logger.info(
` \x1b[32m✅ Prompt optimized\x1b[0m │ \x1b[1m${message}\x1b[0m`
);
}
// File-scoped UI/display methods
clearScreen () {
console.clear();
}
newLine () {
console.log();
}
displayBanner (title, style = 'Standard') {
this.prettyLogger.banner(title, style);
}
displayDivider (char = '\u2500', length = 60, color = 'gray') {
this.prettyLogger.divider(char, length, color);
}
displayBox (content, options = {}) {
this.prettyLogger.box(content, options);
}
displayTable (data, options = {}) {
this.prettyLogger.table(data, options);
}
displayInfo (message) {
this.prettyLogger.info(message);
}
displaySuccess (message) {
this.prettyLogger.success(message);
}
displaySessionHeader () {
this.clearScreen();
this.displayBanner('Nightly Code', 'Standard');
this.displayDivider('\u2550', 60, 'cyan');
this.newLine();
}
displaySessionInfo () {
// Show session info in a styled box
const workingDirDisplay =
this.options.workingDir.length > 45
? `📁 Working Directory:\n ${this.options.workingDir}`
: `📁 Working Directory: ${this.options.workingDir}`;
this.prettyLogger.box(
[
'🌙 Nightly Code Orchestration Session',
'',
`📋 Session ID: ${this.state.sessionId}`,
workingDirDisplay,
`⏱️ Max Duration: ${Math.round(
this.options.maxDuration / 3600
)} hours`,
`🔄 Mode: ${this.options.dryRun ? 'DRY RUN' : 'LIVE'}`
].join('\n'),
{
borderStyle: 'double',
borderColor: this.options.dryRun ? 'yellow' : 'blue',
padding: 1,
align: 'left'
}
);
console.log();
this.state.startTime = Date.now();
}
displayTaskHeader (taskNum, totalTasks, task) {
this.newLine();
this.displayDivider('\u2550', 60, 'blue');
this.displayInfo(
`\ud83d\udccb Task ${taskNum}/${totalTasks}: ${task.title}`
);
this.displayDivider('\u2500', 60, 'gray');
this.displayInfo(`\ud83d\udd27 Type: ${task.type}`);
this.displayInfo(
`\u23f1\ufe0f Minimum duration: ${
task.minimum_duration || 'None specified'
} minutes`
);
this.displayInfo(`\ud83c\udd94 ID: ${task.id}`);
}
displayFinalSummary () {
this.newLine();
this.displayDivider('\u2550', 60, 'cyan');
}
/**
* Initialize logging infrastructure with file and console transports
* Creates log directory structure and configures Winston logger
*
* @private
*/
setupLogging () {
const logDir = path.join(this.options.workingDir, '.nightly-code', 'logs');
fs.ensureDirSync(logDir);
// Custom timestamp format for console
const consoleTimestampFormat = winston.format.timestamp({
format: () => {
const now = new Date();
const time = now.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
return `\x1b[90m[${time}]\x1b[0m`; // Gray color for timestamp
}
});
// Custom format for console output
const consoleFormat = winston.format.printf(
({ level, message, timestamp }) => {
// Add elapsed time since start if available
let elapsedInfo = '';
if (this.state.startTime) {
const elapsed = Math.round(
(Date.now() - this.state.startTime) / TIME.MS.ONE_SECOND
);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
const timeStr =
minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
elapsedInfo = ` \x1b[36m(+${timeStr})\x1b[0m`; // Cyan color for elapsed time
}
return `${timestamp}${elapsedInfo} ${level}: ${message}`;
}
);
this.logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss.SSS'
}),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: path.join(logDir, `${this.state.sessionId}.log`)
}),
new winston.transports.Console({
format: winston.format.combine(
consoleTimestampFormat,
winston.format.colorize({ level: true }),
consoleFormat
)
})
]
});
}
async loadConfigurationFile () {
try {
const configPath = path.resolve(
this.options.workingDir,
this.options.configPath
);
if (await fs.pathExists(configPath)) {
const content = await fs.readFile(configPath, 'utf8');
let config;
if (configPath.endsWith('.yaml') || configPath.endsWith('.yml')) {
config = YAML.parse(content);
} else {
config = JSON.parse(content);
}
// Update rate limiting options from config
if (config.rate_limiting) {
this.options.rateLimitRetries =
config.rate_limiting.max_retries || this.options.rateLimitRetries;
this.options.rateLimitBaseDelay =
config.rate_limiting.base_delay || this.options.rateLimitBaseDelay;
this.options.enableRetryOnLimits =
config.rate_limiting.enabled !== false;
this.options.usageLimitRetry =
config.rate_limiting.usage_limit_retry !== false;
this.options.rateLimitRetry =
config.rate_limiting.rate_limit_retry !== false;
this.options.maxDelay = config.rate_limiting.max_delay || 18000000;
this.options.exponentialBackoff =
config.rate_limiting.exponential_backoff !== false;
this.options.jitter = config.rate_limiting.jitter !== false;
}
// Store SuperClaude configuration
this.superclaudeConfig = config.superclaude || null;
// Apply CLI override if --superclaude flag was used
if (this.options.forceSuperclaude) {
this.superclaudeConfig = {
enabled: true,
planning_mode: 'intelligent',
execution_mode: 'assisted',
task_management: 'hierarchical',
integration_level: 'deep'
};
this.logSessionInfo('Mode enabled via CLI flag');
}
return config;
}
} catch (error) {
this.logWarn(`Could not load configuration file: ${error.message}`);
}
// Apply CLI override even if no config file exists
if (this.options.forceSuperclaude) {
this.superclaudeConfig = {
enabled: true,
planning_mode: 'intelligent',
execution_mode: 'assisted',
task_management: 'hierarchical',
integration_level: 'deep'
};
this.logInfo('🧠 SuperClaude mode enabled via CLI flag');
}
return null;
}
async initializeSuperClaude () {
if (!this.superclaudeConfig?.enabled) {
this.logDebug('SuperClaude integration not enabled');
return;
}
this.superclaudeIntegration = new SuperClaudeIntegration({
enabled: this.superclaudeConfig.enabled,
commandsPath: this.superclaudeConfig.commands_path, // Pass undefined if not specified
workingDir: this.options.workingDir,
logger: this.logger,
planningMode: this.superclaudeConfig.planning_mode || 'intelligent',
executionMode: this.superclaudeConfig.execution_mode || 'assisted',
taskManagement: this.superclaudeConfig.task_management || 'hierarchical',
integrationLevel: this.superclaudeConfig.integration_level || 'deep'
});
await this.superclaudeIntegration.initialize();
}
setupComponents () {
this.taskManager = new TaskManager({
tasksPath: this.options.tasksPath,
logger: this.logger
});
// GitManager will be initialized after config is loaded to get PR strategy
this.gitManager = null;
this.validator = new Validator({
configPath: this.options.configPath,
tasksPath: this.options.tasksPath,
logger: this.logger
});
this.reporter = new Reporter({
workingDir: this.options.workingDir,
logger: this.logger
});
// Initialize SuperClaude integration (will be configured after config load)
this.superclaudeIntegration = null;
}
/**
* Main orchestration method - executes the complete nightly code session
* Coordinates all phases: validation, task execution, git operations, and reporting
*
* @async
* @returns {Promise<Object>} Session results including metrics and task outcomes
* @throws {Error} When critical failures occur during orchestration
*
* @example
* const results = await orchestrator.run();
* console.log(`Completed ${results.completed} tasks, failed ${results.failed}`);
*/
async run () {
try {
this.displaySessionHeader();
this.displaySessionInfo();
this.state.startTime = Date.now();
// Load configuration file
const fullConfig = await this.loadConfigurationFile();
// Initialize GitManager with configuration
this.gitManager = new GitManager({
workingDir: this.options.workingDir,
logger: this.logger,
dryRun: this.options.dryRun,
branchPrefix: fullConfig?.git?.branch_prefix || 'nightly/',
autoPush: fullConfig?.git?.auto_push !== false,
createPR: fullConfig?.git?.create_pr !== false,
prTemplate: fullConfig?.git?.pr_template || null,
prStrategy:
this.options.prStrategy || fullConfig?.git?.pr_strategy || 'task', // Default to task-based PRs
// Dependency-aware branching configuration
dependencyAwareBranching: fullConfig?.git?.dependency_aware_branching !== false, // Default enabled
mergeDependencyChains: fullConfig?.git?.merge_dependency_chains || false,
strictDependencyChecking: fullConfig?.git?.strict_dependency_checking || false
});
// Initialize SuperClaude integration if enabled
await this.initializeSuperClaude();
// Validate configuration and environment
await this.validateEnvironment();
// Load and prepare tasks
const tasks = await this.loadTasks();
// Resume from checkpoint if specified
if (this.options.resumeCheckpoint) {
await this.resumeFromCheckpoint(this.options.resumeCheckpoint);
}
// Start resource monitoring
this.startResourceMonitoring();
// Start checkpoint timer
this.startCheckpointTimer();
// Execute task queue
const results = await this.executeTasks(tasks);
// Cleanup and finalize
await this.finalize(results);
return this.generateFinalReport();
} catch (error) {
this.logError(`💥 Orchestration session failed: ${error.message}`);
await this.handleFailure(error);
throw error;
}
}
async validateEnvironment () {
this.startOperation('environment-validation');
this.displayInfo('🔧 Validating Environment');
this.displayDivider('─', 30, 'gray');
// Check if Claude Code is available
try {
const result = await this.executeCommand('claude', ['--version'], {
timeout: 10000
});
this.logValidationStatus('✅', `Claude Code: ${result.stdout.trim()}`);
} catch (error) {
throw new Error(
'❌ Claude Code CLI not found. Please install claude-code first.'
);
}
// Validate configuration
this.logInfo('🔍 Validating configuration...');
const validation = await this.validator.validateAll();
if (!validation.valid) {
throw new Error(
`❌ Configuration validation failed: ${validation.errors
.map((e) => e.message)
.join(', ')}`
);
}
this.logValidationStatus('✅', 'Configuration is valid');
// Check available disk space
const freeSpace = await this.getAvailableDiskSpace();
const freeSpaceGB = Math.round(freeSpace / STORAGE.BYTES_IN_GB);
if (freeSpace < STORAGE.MIN_DISK_SPACE_BYTES) {
this.logWarn(`⚠️ Low disk space: ${freeSpaceGB}GB available`);
} else {
this.logInfo(`💾 Disk space: ${freeSpaceGB}GB available`);
}
// Validate GitManager is initialized
if (!this.gitManager) {
throw new Error(
'GitManager not initialized. Configuration loading may have failed.'
);
}
// Initialize git if needed
await this.gitManager.ensureRepository();
// Create session branch only if using session PR strategy
if (
!this.options.dryRun &&
this.gitManager.options.prStrategy === 'session'
) {
await this.gitManager.createSessionBranch(this.state.sessionId);
} else if (
!this.options.dryRun &&
this.gitManager.options.prStrategy === 'task'
) {
this.logInfo(
'🌿 Task-based PR strategy - branches will be created per task'
);
} else {
this.logInfo('🔄 Dry run mode - skipping branch creation');
}
this.logWithTiming(
'info',
'✅ Environment validation completed',
'environment-validation'
);
this.logInfo('');
}
async loadTasks () {
this.startOperation('task-loading');
this.displayInfo('📋 Loading Tasks');
this.displayDivider('─', 30, 'gray');
const tasks = await this.taskManager.loadTasks();
const orderedTasks = await this.taskManager.resolveDependencies(tasks);
const totalTasks = orderedTasks.length;
const totalMinimumDuration = orderedTasks.reduce(
(sum, task) => sum + (task.minimum_duration || 0),
0
);
this.displaySuccess(`✅ Loaded ${totalTasks} tasks`);
this.displayInfo(
`⏱️ Total minimum duration: ${Math.round(totalMinimumDuration)} minutes`
);
// Show task overview in a pretty table
if (orderedTasks.length > 0) {
this.newLine();
const tableData = [['#', 'Priority', 'Task', 'Min Duration', 'Type']];
orderedTasks.forEach((task, index) => {
const priority = task.priority || 'medium';
const priorityIcon =
priority === 'high'
? '🔴 High'
: priority === 'low'
? '🟢 Low'
: '🟡 Medium';
tableData.push([
`${index + 1}`,
priorityIcon,
task.title,
task.minimum_duration ? `${task.minimum_duration}m` : 'none',
task.type || 'general'
]);
});
this.displayTable(tableData, {
columnWidths: [4, 12, 40, 10, 12],
align: ['center', 'center', 'left', 'center', 'center']
});
}
this.logWithTiming('info', '', 'task-loading'); // Just show timing without duplicate message
this.newLine();
return orderedTasks;
}
async executeTasks (tasks) {
const results = {
completed: 0,
failed: 0,
skipped: 0,
totalTasks: tasks.length
};
// Build a map of completed tasks for dependency tracking
const completedTasksMap = new Map();
this.logInfo('🎯 Executing Tasks');
this.logInfo('═══════════════════');
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i];
const taskNum = i + 1;
const totalTasks = tasks.length;
try {
// Check time remaining
const elapsed =
(Date.now() - this.state.startTime) / TIME.MS.ONE_SECOND;
if (elapsed >= this.options.maxDuration) {
this.logWarn(
`⏰ Maximum session duration reached (${Math.round(elapsed)}s)`
);
break;
}
this.state.currentTask = task;
const taskOperationName = `task-${task.id}`;
this.startOperation(taskOperationName);
// Task header with pretty display
this.displayTaskHeader(taskNum, totalTasks, task);
// Create task branch with dependency awareness
let taskBranchName = null;
if (!this.options.dryRun) {
taskBranchName = await this.gitManager.createTaskBranch(task, completedTasksMap);
}
// Execute task with Claude Code
const taskResult = await this.executeTask(task);
if (taskResult.success) {
// Validate task completion
const validation = await this.validateTaskCompletion(
task,
taskResult
);
if (validation.passed) {
// Commit changes to task branch (skip in dry-run mode)
if (!this.options.dryRun) {
await this.gitManager.commitTask(task, taskResult);
// Create individual PR if using task-based strategy
if (this.gitManager.options.prStrategy === 'task') {
try {
const prUrl = await this.gitManager.createTaskPR(
task,
taskResult
);
if (prUrl) {
task.prUrl = prUrl;
this.logOperationStatus('🎯', `Task PR created: ${prUrl}`);
} else {
this.logWarn(`⚠️ PR creation skipped for task ${task.id}`);
}
} catch (error) {
this.logError(
`❌ Failed to create PR for task ${task.id}: ${error.message}`
);
// Continue execution, PR creation is not critical for task completion
}
}
} else {
this.logInfo(
'🔄 Dry run mode - skipping task commit and PR creation'
);
}
this.state.completedTasks.push({
task,
result: taskResult,
validation,
completedAt: Date.now(),
prUrl: task.prUrl,
branchName: taskBranchName
});
// Add to completed tasks map for dependency tracking
completedTasksMap.set(task.id, {
taskId: task.id,
branchName: taskBranchName,
completedAt: Date.now()
});
results.completed++;
this.logWithTiming(
'info',
`🎉 Task ${taskNum}/${totalTasks} completed successfully!`,
taskOperationName
);
} else {
throw new Error(
`Task validation failed: ${validation.errors.join(', ')}`
);
}
} else {
throw new Error(`Task execution failed: ${taskResult.error}`);
}
} catch (error) {
this.logError(
`❌ Task ${taskNum}/${totalTasks} failed: ${error.message}`
);
this.state.failedTasks.push({
task,
error: error.message,
failedAt: Date.now()
});
results.failed++;
// Revert to previous state (skip in dry-run mode)
if (!this.options.dryRun) {
await this.gitManager.revertTaskChanges(task);
} else {
this.logInfo('🔄 Dry run mode - skipping task revert');
}
// Continue with next task unless critical failure
if (this.isCriticalFailure(error)) {
this.logError('💥 Critical failure detected, stopping execution');
break;
}
}
this.state.currentTask = null;
// Create checkpoint
await this.createCheckpoint();
}
// Check if all tasks are completed and time remains for automatic improvements
await this.handleAutomaticImprovements(results);
return results;
}
/**
* Handle automatic improvement tasks when all scheduled tasks are completed
* but there is still time remaining in the session
*
* @async
* @param {Object} results - Current session results
*/
async handleAutomaticImprovements (results) {
// Only run automatic improvements if all original tasks completed successfully
if (results.failed > 0) {
this.logInfo('⚠️ Skipping automatic improvements due to failed tasks');
return;
}
// Check remaining time
const elapsed = (Date.now() - this.state.startTime) / TIME.MS.ONE_SECOND;
const remainingTime = this.options.maxDuration - elapsed;
const minimumTimeForImprovement = 300; // 5 minutes minimum
if (remainingTime < minimumTimeForImprovement) {
this.logInfo(
`⏰ Insufficient time remaining for automatic improvements (${Math.round(
remainingTime
)}s < ${minimumTimeForImprovement}s)`
);
return;
}
this.logInfo('');
this.logInfo(
'🚀 All tasks completed successfully! Starting automatic improvements...'
);
this.displayDivider('═', 60, 'green');
this.logInfo(
`⏱️ Time remaining: ${Math.round(remainingTime / 60)} minutes`
);
this.logInfo('');
try {
// Create an automatic improvement task
const improvementTask = await this.createAutomaticImprovementTask(
remainingTime
);
if (improvementTask) {
this.state.currentTask = improvementTask;
this.displayBox(
[
'✨ Automatic Code Improvement Session',
`⏱️ Available time: ${Math.round(remainingTime / 60)} minutes`,
'🎯 Focus: General code quality and optimization'
].join('\n'),
{
borderStyle: 'double',
borderColor: 'green',
padding: 1,
align: 'left'
}
);
// Execute the improvement task
const improvementResult = await this.executeAutomaticImprovementTask(
improvementTask
);
if (improvementResult.success) {
// Validate and commit the improvements
const validation = await this.validateTaskCompletion(
improvementTask,
improvementResult
);
if (validation.passed) {
if (!this.options.dryRun) {
await this.gitManager.commitTask(
improvementTask,
improvementResult
);
this.logValidationStatus(
'✅',
'Automatic improvements committed successfully'
);
} else {
this.logInfo(
'🔄 Dry run mode - skipping automatic improvement commit'
);
}
// Update results
results.completed++;
this.state.completedTasks.push({
task: improvementTask,
result: improvementResult,
validation,
completedAt: Date.now(),
automatic: true
});
this.logOperationStatus(
'🎉',
'Automatic improvement session completed successfully!'
);
} else {
this.logWarn(
'⚠️ Automatic improvement validation failed, reverting changes'
);
if (!this.options.dryRun) {
await this.gitManager.revertTaskChanges(improvementTask);
}
}
} else {
this.logWarn('⚠️ Automatic improvement execution failed');
}
this.state.currentTask = null;
}
} catch (error) {
this.logError(`❌ Automatic improvement failed: ${error.message}`);
this.state.currentTask = null;
}
}
/**
* Create an automatic improvement task based on available time and project state
*
* @async
* @param {number} remainingTime - Remaining session time in seconds
* @returns {Promise<Object>} Generated improvement task
*/
async createAutomaticImprovementTask (remainingTime) {
const improvementDuration = Math.min(remainingTime - 60, 3600); // Leave 1 minute buffer, max 1 hour
return {
id: `auto-improve-${Date.now()}`,
type: 'improvement',
priority: 5,
title: 'Automatic Code Improvement',
requirements: `Perform general code quality improvements and optimizations based on the current codebase state.
Focus areas:
- Code quality and maintainability improvements
- Performance optimizations where applicable
- Documentation enhancements
- Test coverage improvements
- Security best practices
- Code style and convention consistency
Time available: ${Math.round(improvementDuration / 60)} minutes`,
acceptance_criteria: [
'Code quality metrics improved',
'No breaking changes introduced',
'All existing tests continue to pass',
'Changes follow project conventions',
'Improvements are well-documented'
],
minimum_duration: Math.round(improvementDuration / 60),
dependencies: [],
tags: ['automatic', 'improvement', 'quality'],
files_to_modify: [],
enabled: true,
automatic: true,
created_at: new Date(),
updated_at: new Date()
};
}
/**
* Execute automatic improvement task with appropriate command selection
*
* @async
* @param {Object} task - The improvement task to execute
* @returns {Promise<Object>} Task execution result
*/
async executeAutomaticImprovementTask (task) {
let prompt;
// Use SuperClaude improve command if available
if (
this.superclaudeConfig?.enabled &&
this.superclaudeIntegration?.isEnabled()
) {
this.logInfo(
'🧠 Using SuperClaude /sc:improve command for automatic improvements'
);
prompt =
'/sc:improve --scope project --focus quality --iterative --validate';
} else {
// Fallback to standard improvement prompt
this.logInfo('🤖 Using standard improvement approach');
prompt = await this.generateTaskPrompt(task);
}
// Always use standard 60-minute timeout for safety
const timeoutMs = TIME.SECONDS.DEFAULT_TASK_DURATION_MINUTES * 60 * TIME.MS.ONE_SECOND;
try {
const startTime = Date.now();
if (this.options.dryRun) {
this.logInfo(
'🔄 Dry run mode - skipping actual automatic improvement execution'
);
return {
success: true,
output: 'Dry run - automatic improvement task not actually executed',
filesChanged: [],
duration: 0,
automatic: true
};
}
// Execute the improvement with Claude Code
this.logInfo('🚀 Starting automatic improvement task with Claude Code...');
const result = await this.executeClaudeCode(prompt, {
timeout: timeoutMs,
workingDir: this.options.workingDir
});
const duration = Date.now() - startTime;
const durationSeconds = Math.round(duration / TIME.MS.ONE_SECOND);
// Add 30 second delay to allow file system to settle
this.logInfo('⏳ Waiting 30 seconds for file system to settle...');
await new Promise((resolve) => setTimeout(resolve, 30000));
// Analyze changes made by Claude Code
const filesChanged = await this.gitManager.getChangedFiles();
this.logValidationStatus(
'✅',
`Automatic improvement completed in ${durationSeconds}s`
);
if (filesChanged.length > 0) {
this.logInfo(
`📝 ${filesChanged.length} files were modified during improvements`
);
}
return {
success: true,
output: result.stdout,
error: result.stderr,
filesChanged,
duration,
automatic: true
};
} catch (error) {
this.logError(
`💥 Automatic improvement execution failed: ${error.message}`
);
return {
success: false,
error: error.message,
filesChanged: [],
duration: 0,
automatic: true
};
}
}
async executeTask (task) {
// For tasks with minimum_duration, we'll iteratively prompt Claude until minimum time is reached
// IMPORTANT: Each iteration gets the timeout specified by timeout_minutes (or default 60 minutes)
// The minimum_duration controls iteration count, NOT the timeout per iteration
const hasMinimumDuration =
task.minimum_duration && task.minimum_duration > 0;
const minimumDurationMs = task.minimum_duration
? task.minimum_duration * 60 * TIME.MS.ONE_SECOND
: 0;
// Use task-specific timeout if provided, otherwise use default
const baseTimeoutMs = task.timeout_minutes
? task.timeout_minutes * 60 * TIME.MS.ONE_SECOND
: TIME.SECONDS.DEFAULT_TASK_DURATION_MINUTES * 60 * TIME.MS.ONE_SECOND;
const totalStartTime = Date.now();
let totalOutput = '';
let totalFilesChanged = [];
let iterationCount = 0;
let taskCompleted = false;
let claudeSessionId = null; // Track Claude Code session for continuity
const MAX_ITERATIONS = 50; // Safeguard against infinite loops
try {
if (this.options.dryRun) {
this.logInfo('🔄 Dry run mode - simulating Claude Code execution');
// Mock Claude Code execution with spinner updates
spinner.start('🤖 Claude Code is starting...');
await new Promise(resolve => setTimeout(resolve, 1000));
spinner.update('🧠 Analyzing task requirements...');
await new Promise(resolve => setTimeout(resolve, 1500));
spinner.update('📝 Planning execution strategy...');
await new Promise(resolve => setTimeout(resolve, 1000));
spinner.update('⚡ Simulating code generation...');
await new Promise(resolve => setTimeout(resolve, 2000));
spinner.update('🔍 Validating generated code...');
await new Promise(resolve => setTimeout(resolve, 1000));
spinner.succeed('✅ Claude Code dry run completed successfully');
return {
success: true,
output: 'Dry run - task simulation completed',
filesChanged: [],
duration: 6500
};
}
// Enhanced iterative execution with session continuity
do {
iterationCount++;
// Safeguard against infinite loops
if (iterationCount > MAX_ITERATIONS) {
this.logWarn(`⚠️ Maximum iteration limit (${MAX_ITERATIONS}) reached. Stopping execution.`);
taskCompleted = true;
break;
}
const elapsedMs = Date.now() - totalStartTime;
const remainingMs = minimumDurationMs - elapsedMs;
// Check if we should skip this iteration due to time constraints
if (hasMinimumDuration && remainingMs <= 0) {
this.logInfo('⏰ Minimum duration reached, skipping further iterations');
taskCompleted = true;
break;
}
// Use the configured timeout for each iteration
const iterationTimeoutMs = baseTimeoutMs;
const iterationTimeoutMinutes = Math.round(
iterationTimeoutMs / TIME.MS.ONE_MINUTE
);
this.newLine();
this.displayBox(
[
`🤖 Executing task with Claude Code${
hasMinimumDuration ? ` (Iteration ${iterationCount})` : ''
}`,
`⏱️ Timeout: ${iterationTimeoutMinutes} minutes`,
hasMinimumDuration
? `⏳ Minimum duration: ${task.minimum_duration} minutes`
: '',
hasMinimumDuration && elapsedMs > 0
? `⏰ Elapsed: ${Math.round(
elapsedMs / TIME.MS.ONE_MINUTE
)} minutes`
: '',
claudeSessionId
? `🔗 Session: ${claudeSessionId.slice(0, 8)}...`
: '🆕 Starting new session'
]
.filter(Boolean)
.join('\n'),
{
borderStyle: 'single',
borderColor: hasMinimumDuration ? 'magenta' : 'yellow',
padding: 1,
align: 'left'
}
);
let result;
let prompt;
if (iterationCount === 1) {
// First iteration: Generate full prompt and establish session
prompt = await this.generateIterativeTaskPrompt(
task,
iterationCount,
totalOutput,
totalFilesChanged
);
this.logInfo(
`⚡ Starting Claude Code execution (iteration ${iterationCount})...`
);
// Execute with JSON output to capture session ID
result = await this.executeClaudeCodeWithSession(prompt, {
timeout: iterationTimeoutMs,
workingDir: this.options.workingDir,
outputFormat: 'json'
});
// Extract session ID for continuity
if (result.sessionId) {
claudeSessionId = result.sessionId;
this.logInfo(`🔗 Claude Code session established: ${claudeSessionId.slice(0, 8)}...`);
}
} else {
// Subsequent iterations: Use continuation with enhanced prompts
if (claudeSessionId) {
prompt = this.generateContinuationPrompt(
task,
iterationCount,
elapsedMs,
remainingMs,
totalFilesChanged
);
this.logInfo(
`⚡ Continuing Claude Code session (iteration ${iterationCount})...`
);
// Continue existing session with -c flag
result = await this.executeClaudeCodeContinuation(prompt, {
timeout: iterationTimeoutMs,
workingDir: this.options.workingDir,
sessionId: claudeSessionId
});
} else {
// Fallback to regular execution if no session available
this.logWarn('⚠️ No session available, falling back to regular execution');
prompt = await this.generateIterativeTaskPrompt(
task,
iterationCount,
totalOutput,
totalFilesChanged
);
result = await this.executeClaudeCode(prompt, {
timeout: iterationTimeoutMs,
workingDir: this.options.workingDir
});
}
}
const iterationDuration = Date.now() - totalStartTime;
const iterationDurationSeconds = Math.round(
iterationDuration / TIME.MS.ONE_SECOND
);
// Analyze changes made by Claude Code in this iteration
const iterationFilesChanged = await this.gitManager.getChangedFiles();
// Accumulate results
const iterationOutput = result.output || result.stdout || '';
totalOutput +=
(totalOutput ? `\n\n--- Iteration ${iterationCount} ---\n` : '') +
iterationOutput;
// Check for output size limit to prevent memory issues
if (totalOutput.length > STORAGE.MAX_OUTPUT_SIZE) {
const truncateAt = STORAGE.MAX_OUTPUT_SIZE / 2;
totalOutput = `...[Output truncated due to size limit]...\n${
totalOutput.slice(-truncateAt)}`;
this.logWarn('⚠️ Output truncated to prevent memory exhaustion');
}
totalFilesChanged = [
...new Set([...totalFilesChanged, ...iterationFilesChanged])
];
this.logValidationStatus(
'✅',
`Iteration ${iterationCount} completed in ${iterationDurationSeconds}s`
);
if (iterationFilesChanged.length > 0) {
this.logInfo(
`📝 ${iterationFilesChanged.length} files were modified in this iteration`
);
}
// Check if we should continue iterating
const currentElapsedMs = Date.now() - totalStartTime;
const shouldContinue =
hasMinimumDuration &&
currentElapsedMs < minimumDurationMs &&
iterationCount < MAX_ITERATIONS &&
!taskCompleted;
if (shouldContinue) {
const remainingMinutes = Math.round(
(minimumDurationMs - currentElapsedMs) / TIME.MS.ONE_MINUTE
);
this.logInfo(
`🔄 Continuing session - ${remainingMinutes} minutes remaining to meet minimum duration`
);
// Shorter delay for session continuation
await new Promise((resolve) => setTimeout(resolve, 2000));
} else {
taskCompleted = true;
}
} while (!taskCompleted);
const totalDuration = Date.now() - totalStartTime;
const totalDurationSeconds = Math.round(
totalDuration / TIME.MS.ONE_SECOND
);
this.logValidationStatus(
'✅',
`Task execution completed in ${totalDurationSeconds}s (${iterationCount} iterations)`
);
if (totalFilesChanged.length > 0) {
this.logInfo(
`📝 Total ${totalFilesChanged.length} unique files were modified`
);
}
return {
success: true,
output: totalOutput,
error: '',
filesChanged: totalFilesChanged,
duration: totalDuration,
iterations: iterationCount,
sessionId: claudeSessionId
};
} catch (error) {
this.logError(`💥 Claude Code execution failed: ${error.message}`);
return {
success: false,
error: error.message,
filesChanged: totalFilesChanged,
duration: Date.now() - totalStartTime,
iterations: iterationCount,
sessionId: claudeSessionId
};
}
}
async executeClaudeCode (prompt, options = {}) {
// Start spinner for Claude Code execution
spinner.start('🤖 Claude Code is starting...', 'dots12');
try {
// Inform user that Claude Code is starting
this.logInfo('🤖 Claude Code is running...');
// Log the original prompt
this.logPrompt(prompt, 'Original');
// Check if SuperClaude mode is active and optimize prompt
if (
this.superclaudeConfig?.enabled &&
this.superclaudeIntegration?.isEnabled()
) {
spinner.update('🧠 Optimizing prompt with SuperClaude...');
prompt = await this.optimizePromptWithSuperClaude(prompt);
}
spinner.update('⚡ Executing Claude Code task...');
// Check if rate limiting is enabled
if (!this.options.enableRetryOnLimits) {
const result = await this.executeClaudeCodeSingle(prompt, options);
spinner.succeed('✅ Claude Code task completed successfully');
return result;
}
const maxRetries = options.maxRetries || this.options.rateLimitRetries || 5;
const baseDelay =
options.baseDelay ||
this.options.rateLimitBaseDelay ||
TIME.MS.RATE_LIMIT_BASE_DELAY;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await this.executeClaudeCodeSingle(prompt, options);
spinner.succeed('✅ Claude Code task completed successfully');
return result;
} catch (error) {
const errorType = this.classifyError(error);
if (errorType === 'RATE_LIMIT' && this.options.rateLimitRetries > 0) {
if (attempt < maxRetries) {
const delay = this.calculateBackoffDelay(
attempt,
baseDelay,
errorType
);
this.logWarn(
`🔄 Rate limit encountered. Waiting ${Math.round(
delay / TIME.MS.ONE_SECOND
)}s before retry (attempt ${attempt + 1}/${maxRetries})...`
);
spinner.update(`⏳ Waiting ${Math.round(delay / TIME.MS.ONE_SECOND)}s before retry...`);
// Keep session alive during wait
await this.waitWithProgress(delay, errorType);
spinner.update('🔄 Retrying Claude Code task...');
continue;
} else {
this.logError(
`💥 Rate limit exceeded maximum retry attempts (${maxRetries})`
);
throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
}
} else if (
errorType === 'USAGE_LIMIT' &&
this.options.usageLimitRetry
) {
if (attempt < maxRetries) {
const delay = this.calculateBackoffDelay(
attempt,
baseDelay,
errorType
);
const delayMinutes = Math.round(delay / 60000);
const delayHours = Math.floor(delayMinutes / 60);
const remainingMinutes = delayMinutes % 60;
const timeDisplay = delayHours > 0
? `${delayHours}h ${remainingMinutes}m`
: `${delayMinutes} minutes`;
this.logWarn(
`🔄 Usage limit encountered. Waiting ${timeDisplay} before retry (attempt ${attempt + 1}/${maxRetries})...`
);
spinner.update(`⏳ Waiting ${timeDisplay} before retry...`);
// Keep session alive during wait
await this.waitWithProgress(delay, errorType);
spinner.update('🔄 Retrying Claude Code task...');
continue;
} else {
this.logError(
`💥 Usage limit exceeded maximum retry attempts (${maxRetries})`
);
throw new Error(`Usage limit exceeded after ${maxRetries} retries`);
}
} else if (errorType === 'TIMEOUT') {
// Don't retry timeouts, they're usually task-specific
throw error;
} else if (errorType === 'FATAL') {
// Don't retry fatal errors
throw error;
} else {
// For other errors, retry with shorter delay
if (attempt < Math.min(maxRetries, 2)) {
const delay = RETRY.GENERAL_ERROR_DELAY;
this.logWarn(
`⚠️ Execution failed, retrying in ${
delay / TIME.MS.ONE_SECOND
}s (attempt ${attempt + 1}/${maxRetries})...`