@papaoloba/nightly-code-orchestrator
Version:
Automated 8-hour coding sessions using Claude Code
612 lines (511 loc) • 19.2 kB
JavaScript
const yaml = require('yaml');
class TaskDescriptor {
constructor () {
this.taskTypes = {
feature: { priority: 6, duration: 120 },
bugfix: { priority: 8, duration: 90 },
refactor: { priority: 5, duration: 150 },
test: { priority: 6, duration: 120 },
docs: { priority: 3, duration: 60 },
performance: { priority: 7, duration: 120 },
security: { priority: 9, duration: 120 }
};
// Thresholds for automatic task splitting
this.splitThresholds = {
maxLength: 500, // characters
maxComplexity: 8, // complexity score
maxDuration: 300 // minutes
};
this.priorityKeywords = {
critical: 9,
urgent: 9,
high: 8,
medium: 5,
low: 3,
optional: 2
};
this.tags = {
backend: ['api', 'server', 'database', 'endpoint', 'service', 'auth', 'authentication'],
frontend: ['ui', 'component', 'react', 'vue', 'interface', 'display', 'form', 'button'],
security: ['auth', 'authentication', 'encrypt', 'security', 'vulnerability', 'permission'],
performance: ['optimize', 'performance', 'speed', 'memory', 'cache', 'bottleneck'],
testing: ['test', 'testing', 'unit', 'integration', 'e2e', 'coverage'],
documentation: ['doc', 'documentation', 'readme', 'guide', 'api docs', 'comment'],
refactor: ['refactor', 'cleanup', 'reorganize', 'improve', 'maintainability'],
bugfix: ['fix', 'bug', 'issue', 'error', 'crash', 'problem'],
feature: ['implement', 'add', 'create', 'feature', 'functionality', 'capability']
};
}
/**
* Parse natural language description into structured task(s) with automatic splitting
* @param {string} description - Natural language task description
* @returns {Array|Object} Array of tasks if split, single task object otherwise
*/
parseDescription (description) {
// Check if description should be split into multiple tasks
const shouldSplit = this.shouldSplitTask(description);
if (shouldSplit) {
return this.splitTaskDescription(description);
}
return this.parseSingleDescription(description);
}
/**
* Parse a single task description
* @param {string} description - Natural language task description
* @returns {Object} Structured task object
*/
parseSingleDescription (description) {
const lowerDesc = description.toLowerCase();
// Detect task type
const taskType = this.detectTaskType(lowerDesc);
// Extract priority
const priority = this.extractPriority(lowerDesc);
// Extract tags
const tags = this.extractTags(lowerDesc);
// Generate title
const title = this.generateTitle(description, taskType);
// Extract requirements and acceptance criteria
const { requirements, acceptanceCriteria } = this.parseRequirements(description);
// Estimate duration
const estimatedDuration = this.estimateDuration(description, taskType);
// Extract file patterns
const filesToModify = this.extractFilePaths(description);
return {
id: this.generateMeaningfulTaskId(title, taskType),
type: taskType,
priority,
title,
requirements,
acceptance_criteria: acceptanceCriteria,
minimum_duration: estimatedDuration,
dependencies: [],
tags,
files_to_modify: filesToModify,
enabled: true
};
}
detectTaskType (description) {
for (const [type, keywords] of Object.entries(this.tags)) {
if (keywords.some(keyword => description.includes(keyword))) {
// Map tag types to task types
const typeMap = {
bugfix: 'bugfix',
feature: 'feature',
refactor: 'refactor',
testing: 'test',
documentation: 'docs',
performance: 'refactor',
security: 'feature'
};
return typeMap[type] || 'feature';
}
}
return 'feature'; // Default
}
extractPriority (description) {
for (const [keyword, value] of Object.entries(this.priorityKeywords)) {
if (description.includes(keyword)) {
return value;
}
}
// Check for specific indicators
if (description.includes('asap') || description.includes('immediately')) {
return 9;
}
return 5; // Default medium priority
}
extractTags (description) {
const foundTags = [];
for (const [tag, keywords] of Object.entries(this.tags)) {
if (keywords.some(keyword => description.includes(keyword))) {
foundTags.push(tag);
}
}
return [...new Set(foundTags)]; // Remove duplicates
}
generateTitle (description, taskType) {
// Extract first sentence or up to 80 characters
const firstSentence = description.split(/[.!?]/)[0].trim();
const title = firstSentence.length > 80
? `${firstSentence.substring(0, 77)}...`
: firstSentence;
// Capitalize first letter
return title.charAt(0).toUpperCase() + title.slice(1);
}
parseRequirements (description) {
const lines = description.split('\n').map(line => line.trim()).filter(line => line);
let requirements = [];
let acceptanceCriteria = [];
let currentSection = 'requirements';
for (const line of lines) {
// Check for section markers
if (line.toLowerCase().includes('acceptance') ||
line.toLowerCase().includes('criteria') ||
line.toLowerCase().includes('should') ||
line.toLowerCase().includes('must')) {
currentSection = 'criteria';
}
// Extract bullet points or numbered items
if (line.match(/^[-*•]\s/) || line.match(/^\d+[.)]\s/)) {
const item = line.replace(/^[-*•]\s/, '').replace(/^\d+[.)]\s/, '').trim();
if (currentSection === 'criteria') {
acceptanceCriteria.push(item);
} else {
requirements.push(item);
}
}
}
// If no structured items found, use the full description as requirements
if (requirements.length === 0) {
requirements = description;
} else {
requirements = requirements.join('\n');
}
// Generate acceptance criteria if none found
if (acceptanceCriteria.length === 0) {
acceptanceCriteria = this.generateAcceptanceCriteria(description);
}
return { requirements, acceptanceCriteria };
}
generateAcceptanceCriteria (description) {
const criteria = [];
// Extract action items
const actionWords = ['implement', 'add', 'create', 'fix', 'update', 'refactor', 'test'];
for (const word of actionWords) {
const regex = new RegExp(`${word}\\s+([^,.]+)`, 'gi');
const matches = description.matchAll(regex);
for (const match of matches) {
criteria.push(`${word.charAt(0).toUpperCase() + word.slice(1)} ${match[1].trim()} is completed`);
}
}
// Add standard criteria based on task type
if (description.includes('test')) {
criteria.push('All tests pass successfully');
criteria.push('Test coverage meets requirements');
}
if (description.includes('api') || description.includes('endpoint')) {
criteria.push('API endpoints return proper HTTP status codes');
criteria.push('API documentation is updated');
}
if (description.includes('ui') || description.includes('component')) {
criteria.push('UI components render correctly');
criteria.push('User interactions work as expected');
}
// Always add these
criteria.push('Code follows project conventions');
criteria.push('No regression issues introduced');
return criteria;
}
estimateDuration (description, taskType) {
const baseDuration = this.taskTypes[taskType]?.duration || 120;
// Adjust based on complexity indicators
let multiplier = 1;
if (description.includes('complex') || description.includes('comprehensive')) {
multiplier *= 1.5;
}
if (description.includes('simple') || description.includes('minor')) {
multiplier *= 0.7;
}
// Count number of features/items
const itemCount = (description.match(/[-*]\s/g) || []).length;
if (itemCount > 5) {
multiplier *= 1.3;
}
return Math.round(baseDuration * multiplier);
}
extractFilePaths (description) {
const filePaths = [];
// Look for file path patterns
const pathRegex = /(?:src\/|test\/|lib\/|components\/|pages\/|api\/|routes\/)[^\s,]+/g;
const matches = description.matchAll(pathRegex);
for (const match of matches) {
filePaths.push(match[0]);
}
// Add common paths based on keywords
if (description.includes('api') || description.includes('endpoint')) {
filePaths.push('src/api/', 'src/routes/');
}
if (description.includes('component') || description.includes('ui')) {
filePaths.push('src/components/');
}
if (description.includes('test')) {
filePaths.push('test/', 'src/__tests__/');
}
return [...new Set(filePaths)]; // Remove duplicates
}
/**
* Generate meaningful task ID based on title and type
* @param {string} title - Task title
* @param {string} type - Task type
* @returns {string} Meaningful task ID
*/
generateMeaningfulTaskId (title, type) {
// Extract key words from title
const titleWords = title
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '') // Remove special chars
.split(/\s+/)
.filter(word => word.length > 2) // Filter short words
.slice(0, 3); // Take first 3 meaningful words
const titlePart = titleWords.join('-') || 'task';
const timestamp = Date.now().toString().slice(-6); // Last 6 digits
return `${type}-${titlePart}-${timestamp}`;
}
/**
* Check if a task description should be split into multiple tasks
* @param {string} description - Task description
* @returns {boolean} Whether to split the task
*/
shouldSplitTask (description) {
// Check length threshold
if (description.length > this.splitThresholds.maxLength) {
return true;
}
// Check for multiple distinct tasks (bullet points, numbered lists)
const bulletPoints = (description.match(/^\s*[-*•]\s/gm) || []).length;
const numberedItems = (description.match(/^\s*\d+[.)]/gm) || []).length;
const distinctTasks = Math.max(bulletPoints, numberedItems);
if (distinctTasks > 3) {
return true;
}
// Check for multiple domains/areas
const domainCount = Object.keys(this.tags).filter(domain =>
this.tags[domain].some(keyword => description.toLowerCase().includes(keyword))
).length;
if (domainCount > 2) {
return true;
}
// Check complexity indicators
const complexityScore = this.calculateComplexityScore(description);
if (complexityScore > this.splitThresholds.maxComplexity) {
return true;
}
return false;
}
/**
* Calculate complexity score for a task description
* @param {string} description - Task description
* @returns {number} Complexity score (0-10)
*/
calculateComplexityScore (description) {
let score = 0;
const lowerDesc = description.toLowerCase();
// Length factor
score += Math.min(description.length / 100, 3);
// Complexity keywords
const complexityKeywords = [
'complex', 'comprehensive', 'integrate', 'refactor', 'migrate',
'architecture', 'system', 'multiple', 'advanced', 'sophisticated'
];
score += complexityKeywords.filter(keyword => lowerDesc.includes(keyword)).length;
// Technology count
const technologies = [
'react', 'vue', 'angular', 'node', 'express', 'database', 'api',
'authentication', 'testing', 'deployment', 'docker', 'kubernetes'
];
score += technologies.filter(tech => lowerDesc.includes(tech)).length * 0.5;
// Action verbs count
const actionVerbs = [
'implement', 'create', 'build', 'develop', 'design', 'integrate',
'optimize', 'refactor', 'test', 'deploy', 'configure', 'setup'
];
score += actionVerbs.filter(verb => lowerDesc.includes(verb)).length * 0.3;
return Math.min(score, 10);
}
/**
* Split a complex task description into multiple tasks
* @param {string} description - Complex task description
* @returns {Array} Array of task objects
*/
splitTaskDescription (description) {
// Strategy 1: Split by bullet points or numbered lists
const bulletTasks = this.splitByBulletPoints(description);
if (bulletTasks.length > 1) {
return bulletTasks;
}
// Strategy 2: Split by sentences with action verbs
const sentenceTasks = this.splitBySentences(description);
if (sentenceTasks.length > 1) {
return sentenceTasks;
}
// Strategy 3: Split by domain/technology areas
const domainTasks = this.splitByDomains(description);
if (domainTasks.length > 1) {
return domainTasks;
}
// Fallback: Create phases for large tasks
return this.createPhases(description);
}
/**
* Split description by bullet points or numbered lists
* @param {string} description - Task description
* @returns {Array} Array of task objects
*/
splitByBulletPoints (description) {
const lines = description.split('\n');
const tasks = [];
let currentTask = '';
let mainDescription = '';
for (const line of lines) {
const trimmedLine = line.trim();
// Check if line is a bullet point or numbered item
if (trimmedLine.match(/^[-*•]\s/) || trimmedLine.match(/^\d+[.)]\s/)) {
// Save previous task if exists
if (currentTask.trim()) {
tasks.push(this.parseSingleDescription(`${mainDescription}\n${currentTask}`));
}
// Start new task
currentTask = trimmedLine.replace(/^[-*•]\s/, '').replace(/^\d+[.)]\s/, '');
} else if (currentTask) {
// Continue current task
currentTask += `\n${trimmedLine}`;
} else {
// Part of main description
mainDescription += `\n${trimmedLine}`;
}
}
// Add last task
if (currentTask.trim()) {
tasks.push(this.parseSingleDescription(`${mainDescription}\n${currentTask}`));
}
return tasks.length > 1 ? tasks : [];
}
/**
* Split description by sentences with action verbs
* @param {string} description - Task description
* @returns {Array} Array of task objects
*/
splitBySentences (description) {
const sentences = description.split(/[.!?]/).map(s => s.trim()).filter(s => s);
const actionVerbs = [
'implement', 'create', 'build', 'develop', 'design', 'add',
'update', 'fix', 'refactor', 'test', 'deploy', 'configure'
];
const actionSentences = sentences.filter(sentence =>
actionVerbs.some(verb => sentence.toLowerCase().includes(verb))
);
if (actionSentences.length > 2) {
return actionSentences.map(sentence => this.parseSingleDescription(sentence));
}
return [];
}
/**
* Split description by different technology domains
* @param {string} description - Task description
* @returns {Array} Array of task objects
*/
splitByDomains (description) {
const domainSections = {};
const lowerDesc = description.toLowerCase();
// Group content by domains
for (const [domain, keywords] of Object.entries(this.tags)) {
const matchingKeywords = keywords.filter(keyword => lowerDesc.includes(keyword));
if (matchingKeywords.length > 0) {
domainSections[domain] = {
keywords: matchingKeywords,
content: description // For now, use full description
};
}
}
// If multiple domains found, create separate tasks
const domains = Object.keys(domainSections);
if (domains.length > 2) {
return domains.map(domain => {
const domainContent = `${domain.charAt(0).toUpperCase() + domain.slice(1)} work: ${description}`;
return this.parseSingleDescription(domainContent);
});
}
return [];
}
/**
* Create phases for large tasks
* @param {string} description - Task description
* @returns {Array} Array of task objects representing phases
*/
createPhases (description) {
const phases = [
'Planning and Analysis',
'Implementation',
'Testing and Validation',
'Documentation and Cleanup'
];
return phases.map((phase, index) => {
const phaseDescription = `${phase}: ${description}`;
const task = this.parseSingleDescription(phaseDescription);
task.title = `${phase} - ${task.title}`;
task.dependencies = index > 0 ? [phases[index - 1].toLowerCase().replace(/\s+/g, '-')] : [];
return task;
});
}
/**
* Convert multiple descriptions to optimized nightly-tasks.yaml
* @param {Array<string>} descriptions - Array of task descriptions
* @returns {string} YAML content
*/
generateTasksYaml (descriptions) {
let allTasks = [];
// Process each description, handling potential task splitting
for (const desc of descriptions) {
const result = this.parseDescription(desc);
if (Array.isArray(result)) {
allTasks = allTasks.concat(result);
} else {
allTasks.push(result);
}
}
// Sort by priority, then by dependencies
allTasks.sort((a, b) => {
if (a.priority !== b.priority) {
return b.priority - a.priority;
}
// Tasks with no dependencies come first
return a.dependencies.length - b.dependencies.length;
});
const yamlContent = {
version: '1.0',
created_at: new Date().toISOString(),
tasks: allTasks,
metadata: {
total_tasks: allTasks.length,
estimated_total_duration: allTasks.reduce((sum, task) => sum + (task.minimum_duration || 0), 0),
auto_split_applied: allTasks.some(task => task.title.includes(' - ') || task.dependencies.length > 0)
}
};
return yaml.stringify(yamlContent, {
indent: 2,
lineWidth: 0
});
}
/**
* Optimize and validate task configuration
* @param {Object} task - Task object
* @returns {Object} Optimized task
*/
optimizeTask (task) {
// Ensure all required fields
const optimized = {
id: task.id || this.generateMeaningfulTaskId(task.title || 'Untitled', task.type || 'feature'),
type: task.type || 'feature',
priority: Math.min(Math.max(task.priority || 5, 1), 10),
title: task.title || 'Untitled Task',
requirements: task.requirements || '',
acceptance_criteria: Array.isArray(task.acceptance_criteria)
? task.acceptance_criteria
: [task.acceptance_criteria || 'Task completed successfully'].flat(),
minimum_duration: task.minimum_duration || 0,
dependencies: task.dependencies || [],
tags: task.tags || [],
files_to_modify: task.files_to_modify || [],
enabled: task.enabled !== false
};
// Add custom validation if complex task
if (optimized.priority >= 8 || (optimized.minimum_duration && optimized.minimum_duration > 180)) {
optimized.custom_validation = {
script: './scripts/validate-task.js',
timeout: 300
};
}
return optimized;
}
}
module.exports = { TaskDescriptor };