aetherlight-analyzer
Version:
Code analysis tool to generate ÆtherLight sprint plans from any codebase
496 lines • 18.9 kB
JavaScript
;
/**
* Sprint Executor
*
* DESIGN DECISION: Coordinate autonomous sprint execution with Claude Code agents
* WHY: Manual sprint execution is slow - automate with multi-agent system
*
* REASONING CHAIN:
* 1. Read generated sprint plan (PHASE_A/B/C.md)
* 2. Parse tasks with dependencies
* 3. Spawn Claude Code agents per task (parallel where possible)
* 4. Track progress with TodoWrite integration
* 5. Generate git commits with Chain of Thought
* 6. Result: Autonomous sprint completion in hours (not days)
*
* ARCHITECTURE:
* - SprintExecutor: Main orchestrator
* - TaskQueue: Manages task execution order (topological sort)
* - AgentPool: Spawns and monitors Claude Code agents
* - ProgressTracker: Real-time progress updates
* - CommitGenerator: Creates git commits with CoT
*
* PATTERN: Pattern-EXECUTOR-001 (Autonomous Sprint Execution)
* RELATED: Task C-002 (Sprint Executor), Phase 4 (Autonomous Sprints)
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SprintExecutor = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const events_1 = require("events");
/**
* Sprint Executor
*
* Coordinates autonomous sprint execution with multi-agent system
*/
class SprintExecutor extends events_1.EventEmitter {
options;
runningAgents;
completedTasks;
constructor(options = {}) {
super();
this.options = {
maxParallelAgents: options.maxParallelAgents ?? 3,
verbose: options.verbose ?? false,
dryRun: options.dryRun ?? false,
stopOnError: options.stopOnError ?? true,
};
this.runningAgents = new Map();
this.completedTasks = new Set();
}
/**
* Execute sprint plan
*
* DESIGN DECISION: Topological execution with parallel agents
* WHY: Maximize throughput while respecting dependencies
*
* REASONING CHAIN:
* 1. Load sprint plan from markdown file
* 2. Parse tasks and dependencies
* 3. Build execution graph (topological sort)
* 4. Execute tasks in waves (parallel where possible)
* 5. Track progress and handle errors
* 6. Generate final report
*/
async executeSprint(sprintFilePath) {
this.emit('progress', {
type: 'sprint-started',
message: `Starting sprint execution: ${sprintFilePath}`,
timestamp: new Date(),
});
// Stage 1: Load sprint plan
const sprintPlan = await this.loadSprintPlan(sprintFilePath);
if (this.options.verbose) {
console.log(`\n=� Sprint Plan: ${sprintPlan.title}`);
console.log(`Tasks: ${sprintPlan.tasks.length}`);
console.log(`Estimated Duration: ${sprintPlan.estimatedDuration}\n`);
}
// Stage 2: Validate dependencies
this.validateDependencies(sprintPlan.tasks);
// Stage 3: Build execution order
const executionOrder = this.buildExecutionOrder(sprintPlan.tasks);
if (this.options.dryRun) {
console.log('\nDRY RUN - Execution Order:');
executionOrder.forEach((taskId, index) => {
const task = sprintPlan.tasks.find((t) => t.id === taskId);
console.log(` ${index + 1}. ${task.id}: ${task.title} (${task.duration})`);
});
return;
}
// Stage 4: Execute tasks
try {
await this.executeTasks(sprintPlan.tasks, executionOrder);
this.emit('progress', {
type: 'sprint-completed',
message: `Sprint completed successfully: ${sprintPlan.tasks.length} tasks`,
timestamp: new Date(),
});
}
catch (error) {
this.emit('progress', {
type: 'sprint-failed',
message: `Sprint failed: ${error}`,
timestamp: new Date(),
});
throw error;
}
}
/**
* Load sprint plan from markdown file
*
* DESIGN DECISION: Parse markdown to extract task structure
* WHY: Sprint plans are markdown - need structured representation
*/
async loadSprintPlan(filePath) {
const content = await fs.readFile(filePath, 'utf-8');
// Extract phase from filename (PHASE_A_ENHANCEMENT.md � A)
const fileName = path.basename(filePath);
const phaseMatch = fileName.match(/PHASE_([ABC])_/);
const phase = phaseMatch ? phaseMatch[1] : 'A';
// Parse title (first h1)
const titleMatch = content.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1] : 'Sprint Plan';
// Parse description (content between title and first task)
const descMatch = content.match(/^# .+$\n\n(.+?)(?=\n## Task)/ms);
const description = descMatch ? descMatch[1].trim() : '';
// Parse estimated duration
const durationMatch = content.match(/\*\*Estimated Duration:\*\* (.+)/);
const estimatedDuration = durationMatch ? durationMatch[1] : 'Unknown';
// Parse tasks
const tasks = this.parseTasks(content, phase);
return {
phase,
title,
description,
tasks,
estimatedDuration,
};
}
/**
* Parse tasks from markdown content
*/
parseTasks(content, phase) {
const tasks = [];
// Match task sections: ## Task A-001: Title (capture entire section until next task or end)
const taskRegex = /## Task ([ABC]-\d{3}): (.+?)(?=\n## Task|\n---\n\n\*\*STATUS|$)/gs;
let match;
while ((match = taskRegex.exec(content)) !== null) {
const taskId = match[1];
const taskTitle = match[2].trim();
const taskContent = match[0];
// Extract agent
const agentMatch = taskContent.match(/\*\*Agent:\*\* (.+?)(?:\n|$)/);
const agent = agentMatch ? agentMatch[1].trim() : 'general-agent';
// Extract duration
const durationMatch = taskContent.match(/\*\*Duration:\*\* (.+?)(?:\n|$)/);
const duration = durationMatch ? durationMatch[1].trim() : 'Unknown';
// Extract dependencies
const depsMatch = taskContent.match(/\*\*Dependencies:\*\* (.+?)(?:\n|$)/);
const dependencies = [];
if (depsMatch) {
const depsStr = depsMatch[1].trim();
if (depsStr !== 'None' && depsStr !== '') {
dependencies.push(...depsStr.split(',').map((d) => d.trim()));
}
}
// Extract validation criteria
const validationCriteria = [];
const criteriaRegex = /- \[ \] (.+)/g;
let criteriaMatch;
while ((criteriaMatch = criteriaRegex.exec(taskContent)) !== null) {
validationCriteria.push(criteriaMatch[1].trim());
}
// Extract description (content between title line and first **Field:** marker)
const descMatch = taskContent.match(/## Task [ABC]-\d{3}: .+?\n\n(.+?)(?=\n\*\*[A-Z]|$)/s);
const description = descMatch ? descMatch[1].trim() : '';
tasks.push({
id: taskId,
title: taskTitle,
description,
agent,
duration,
dependencies,
validationCriteria,
completed: false,
});
}
return tasks;
}
/**
* Validate dependencies
*
* DESIGN DECISION: Fail fast if dependencies are invalid
* WHY: Circular dependencies or missing tasks = unexecutable sprint
*/
validateDependencies(tasks) {
const taskIds = new Set(tasks.map((t) => t.id));
// Check for missing dependencies
for (const task of tasks) {
for (const dep of task.dependencies) {
if (!taskIds.has(dep)) {
throw new Error(`Task ${task.id} depends on non-existent task ${dep}`);
}
}
}
// Check for circular dependencies (DFS)
const visited = new Set();
const recursionStack = new Set();
const hasCycle = (taskId) => {
visited.add(taskId);
recursionStack.add(taskId);
const task = tasks.find((t) => t.id === taskId);
if (!task)
return false;
for (const dep of task.dependencies) {
if (!visited.has(dep)) {
if (hasCycle(dep))
return true;
}
else if (recursionStack.has(dep)) {
return true;
}
}
recursionStack.delete(taskId);
return false;
};
for (const task of tasks) {
if (!visited.has(task.id)) {
if (hasCycle(task.id)) {
throw new Error(`Circular dependency detected involving task ${task.id}`);
}
}
}
}
/**
* Build execution order using topological sort
*
* DESIGN DECISION: Kahn's algorithm for topological sort
* WHY: Proven algorithm, O(V+E) complexity, handles dependencies correctly
*/
buildExecutionOrder(tasks) {
const graph = new Map();
const inDegree = new Map();
// Build dependency graph
for (const task of tasks) {
graph.set(task.id, task.dependencies);
inDegree.set(task.id, task.dependencies.length);
}
// Kahn's algorithm
const queue = [];
const sorted = [];
// Find tasks with no dependencies
inDegree.forEach((degree, taskId) => {
if (degree === 0) {
queue.push(taskId);
}
});
while (queue.length > 0) {
const taskId = queue.shift();
sorted.push(taskId);
// For each task that depends on this completed task
tasks.forEach((task) => {
if (task.dependencies.includes(taskId)) {
const degree = inDegree.get(task.id) - 1;
inDegree.set(task.id, degree);
if (degree === 0) {
queue.push(task.id);
}
}
});
}
if (sorted.length !== tasks.length) {
throw new Error('Topological sort failed - circular dependency detected');
}
return sorted;
}
/**
* Execute tasks in order
*
* DESIGN DECISION: Wave-based execution with parallel agents
* WHY: Maximize throughput while respecting dependencies
*
* REASONING CHAIN:
* 1. Group tasks into waves (tasks with same dependency depth)
* 2. Execute each wave in parallel (up to maxParallelAgents)
* 3. Wait for wave to complete before starting next
* 4. Track progress and handle errors
* 5. Result: Optimal execution time with correct ordering
*/
async executeTasks(tasks, executionOrder) {
// Group tasks into waves
const waves = this.groupIntoWaves(tasks, executionOrder);
if (this.options.verbose) {
console.log(`\nExecution waves: ${waves.length}`);
waves.forEach((wave, index) => {
console.log(` Wave ${index + 1}: ${wave.length} tasks`);
});
console.log('');
}
// Execute each wave
for (let waveIndex = 0; waveIndex < waves.length; waveIndex++) {
const wave = waves[waveIndex];
if (this.options.verbose) {
console.log(`\nWave ${waveIndex + 1}/${waves.length}: ${wave.length} tasks`);
}
await this.executeWave(tasks, wave);
}
}
/**
* Group tasks into waves based on dependencies
*/
groupIntoWaves(tasks, executionOrder) {
const waves = [];
const taskDepth = new Map();
// Calculate depth for each task (max depth of dependencies + 1)
const calculateDepth = (taskId) => {
if (taskDepth.has(taskId)) {
return taskDepth.get(taskId);
}
const task = tasks.find((t) => t.id === taskId);
if (task.dependencies.length === 0) {
taskDepth.set(taskId, 0);
return 0;
}
const maxDepDep = Math.max(...task.dependencies.map(calculateDepth));
const depth = maxDepDep + 1;
taskDepth.set(taskId, depth);
return depth;
};
// Calculate depths
for (const taskId of executionOrder) {
calculateDepth(taskId);
}
// Group by depth
const maxDepth = Math.max(...Array.from(taskDepth.values()));
for (let depth = 0; depth <= maxDepth; depth++) {
const wave = executionOrder.filter((taskId) => taskDepth.get(taskId) === depth);
if (wave.length > 0) {
waves.push(wave);
}
}
return waves;
}
/**
* Execute a wave of tasks in parallel
*/
async executeWave(tasks, wave) {
const promises = [];
for (const taskId of wave) {
const task = tasks.find((t) => t.id === taskId);
// Wait if too many agents running
while (this.runningAgents.size >= this.options.maxParallelAgents) {
await this.delay(1000);
}
promises.push(this.executeTask(task));
}
// Wait for all tasks in wave to complete
await Promise.all(promises);
}
/**
* Execute a single task
*
* DESIGN DECISION: Spawn Claude Code agent as subprocess
* WHY: Isolated execution, real-time monitoring, graceful failure handling
*/
async executeTask(task) {
this.emit('progress', {
type: 'task-started',
taskId: task.id,
message: `Starting task ${task.id}: ${task.title}`,
timestamp: new Date(),
});
task.startTime = new Date();
try {
// In production, this would spawn actual Claude Code agent
// For now, simulate execution with delay
if (this.options.verbose) {
console.log(` � ${task.id}: ${task.title} (${task.agent})`);
}
// Simulate task execution (in production: spawn Claude Code subprocess)
await this.simulateTaskExecution(task);
task.completed = true;
task.endTime = new Date();
this.completedTasks.add(task.id);
this.emit('progress', {
type: 'task-completed',
taskId: task.id,
message: `Completed task ${task.id}: ${task.title}`,
timestamp: new Date(),
});
if (this.options.verbose) {
const duration = task.endTime.getTime() - task.startTime.getTime();
console.log(` ${task.id}: Completed in ${(duration / 1000).toFixed(1)}s`);
}
}
catch (error) {
task.error = String(error);
task.endTime = new Date();
this.emit('progress', {
type: 'task-failed',
taskId: task.id,
message: `Failed task ${task.id}: ${error}`,
timestamp: new Date(),
});
if (this.options.verbose) {
console.error(` L ${task.id}: Failed - ${error}`);
}
if (this.options.stopOnError) {
throw new Error(`Task ${task.id} failed: ${error}`);
}
}
}
/**
* Simulate task execution (placeholder for actual Claude Code agent spawn)
*/
async simulateTaskExecution(task) {
// Parse duration to milliseconds
const durationMs = this.parseDuration(task.duration);
// Simulate work with delay
await this.delay(durationMs);
// Note: Random failures removed for deterministic tests
// In production, actual task failures would be caught from subprocess
}
/**
* Parse duration string to milliseconds
*/
parseDuration(duration) {
const match = duration.match(/(\d+)\s*(hour|day|minute)s?/);
if (!match)
return 10; // Default: 10ms
const value = parseInt(match[1], 10);
const unit = match[2];
// For simulation, scale down dramatically for fast tests
switch (unit) {
case 'minute':
return value * 1; // 1 minute = 1ms
case 'hour':
return value * 10; // 1 hour = 10ms
case 'day':
return value * 100; // 1 day = 100ms
default:
return 10;
}
}
/**
* Delay helper
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Get execution summary
*/
getExecutionSummary() {
return {
totalTasks: this.completedTasks.size + this.runningAgents.size,
completedTasks: this.completedTasks.size,
failedTasks: 0, // Would track failed tasks in production
runningTasks: this.runningAgents.size,
};
}
}
exports.SprintExecutor = SprintExecutor;
//# sourceMappingURL=sprint-executor.js.map