UNPKG

sf-agent-framework

Version:

AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction

602 lines (506 loc) • 16.6 kB
#!/usr/bin/env node /** * Story Queue Manager for SF-Agent Framework * Manages the queue of implementation stories during development phase * Tracks story status, dependencies, and progress */ const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const yaml = require('js-yaml'); class StoryQueueManager { constructor(options = {}) { this.options = { storyDir: options.storyDir || 'docs/stories', queueFile: options.queueFile || 'docs/stories/story-queue.yaml', statusFile: options.statusFile || 'docs/stories/story-status.yaml', verbose: options.verbose || false, ...options, }; this.queue = []; this.status = {}; this.currentStory = null; this.storyStates = { PENDING: 'pending', IN_PROGRESS: 'in_progress', IN_REVIEW: 'in_review', COMPLETED: 'completed', BLOCKED: 'blocked', SKIPPED: 'skipped', }; } /** * Initialize story queue from sharded documents */ async initializeQueue() { console.log(chalk.blue('šŸŽÆ Initializing story queue...')); try { // Load existing queue if exists if (await fs.pathExists(this.options.queueFile)) { await this.loadQueue(); console.log(chalk.yellow(` šŸ“‹ Loaded existing queue with ${this.queue.length} stories`)); } else { // Build new queue from story files await this.buildQueueFromStories(); } // Load or initialize status await this.loadStatus(); return this.queue; } catch (error) { console.error(chalk.red(`āŒ Error initializing queue: ${error.message}`)); throw error; } } /** * Build queue from story files in directory */ async buildQueueFromStories() { console.log(chalk.cyan(' šŸ“ Scanning for story files...')); const storyFiles = await fs.readdir(this.options.storyDir); const stories = []; for (const file of storyFiles) { if (file.endsWith('.md') && file.startsWith('STORY-')) { const filepath = path.join(this.options.storyDir, file); const story = await this.parseStoryFile(filepath); if (story) { stories.push(story); } } } // Sort stories by priority and dependencies this.queue = this.sortStories(stories); // Save queue await this.saveQueue(); console.log(chalk.green(` āœ… Created queue with ${this.queue.length} stories`)); } /** * Parse story file to extract metadata */ async parseStoryFile(filepath) { try { const content = await fs.readFile(filepath, 'utf-8'); const lines = content.split('\n'); const story = { id: path.basename(filepath, '.md'), file: filepath, title: '', epic: '', priority: 'P2', sprint: null, estimated_hours: 0, dependencies: [], status: this.storyStates.PENDING, assigned_to: null, created_date: new Date().toISOString(), metadata: {}, }; // Parse story metadata from content let inMetadata = false; for (const line of lines) { // Extract title if (line.startsWith('# STORY-')) { const match = line.match(/# (STORY-[\d-]+):\s*(.+)/); if (match) { story.id = match[1]; story.title = match[2]; } } // Parse metadata section if (line.includes('## Story Metadata') || line.includes('## Metadata')) { inMetadata = true; continue; } if (inMetadata) { if (line.startsWith('## ')) { inMetadata = false; continue; } // Extract metadata fields if (line.includes('Epic:')) { story.epic = line.split('Epic:')[1].trim(); } if (line.includes('Priority:')) { story.priority = line.split('Priority:')[1].trim(); } if (line.includes('Sprint:')) { story.sprint = line.split('Sprint:')[1].trim(); } if (line.includes('Estimated:')) { const estimate = line.split('Estimated:')[1].trim(); story.estimated_hours = parseFloat(estimate) || 0; } if (line.includes('Dependencies:')) { const deps = line.split('Dependencies:')[1].trim(); story.dependencies = deps.split(',').map((d) => d.trim()); } if (line.includes('Assigned:')) { story.assigned_to = line.split('Assigned:')[1].trim(); } } } return story; } catch (error) { console.error(chalk.red(` āš ļø Error parsing story ${filepath}: ${error.message}`)); return null; } } /** * Sort stories by priority and dependencies */ sortStories(stories) { // Priority order const priorityOrder = { P0: 0, P1: 1, P2: 2, P3: 3 }; // Sort by priority first stories.sort((a, b) => { const aPriority = priorityOrder[a.priority] || 99; const bPriority = priorityOrder[b.priority] || 99; return aPriority - bPriority; }); // Resolve dependencies const sorted = []; const added = new Set(); const addStory = (story) => { if (added.has(story.id)) return; // Add dependencies first for (const depId of story.dependencies) { const dep = stories.find((s) => s.id === depId); if (dep) { addStory(dep); } } // Add the story sorted.push(story); added.add(story.id); }; // Add all stories respecting dependencies for (const story of stories) { addStory(story); } return sorted; } /** * Get next story from queue */ async getNextStory() { // Find next pending story const nextStory = this.queue.find( (story) => story.status === this.storyStates.PENDING && this.areDependenciesMet(story) ); if (!nextStory) { console.log(chalk.yellow('šŸ“­ No available stories in queue')); // Check for blocked stories const blocked = this.queue.filter((s) => s.status === this.storyStates.BLOCKED); if (blocked.length > 0) { console.log(chalk.red(` āš ļø ${blocked.length} stories are blocked`)); } return null; } // Update status nextStory.status = this.storyStates.IN_PROGRESS; nextStory.started_date = new Date().toISOString(); this.currentStory = nextStory; // Save status await this.saveStatus(); console.log(chalk.green(`\nšŸ“‹ Next Story: ${nextStory.id}`)); console.log(` Title: ${nextStory.title}`); console.log(` Priority: ${nextStory.priority}`); console.log(` Estimated: ${nextStory.estimated_hours} hours`); return nextStory; } /** * Check if story dependencies are met */ areDependenciesMet(story) { if (!story.dependencies || story.dependencies.length === 0) { return true; } for (const depId of story.dependencies) { const dep = this.queue.find((s) => s.id === depId); if (!dep || dep.status !== this.storyStates.COMPLETED) { return false; } } return true; } /** * Update story status */ async updateStoryStatus(storyId, newStatus, notes = '') { const story = this.queue.find((s) => s.id === storyId); if (!story) { throw new Error(`Story ${storyId} not found in queue`); } const oldStatus = story.status; story.status = newStatus; story.last_updated = new Date().toISOString(); // Add status transition if (!story.status_history) { story.status_history = []; } story.status_history.push({ from: oldStatus, to: newStatus, date: new Date().toISOString(), notes: notes, }); // Set completion date if completed if (newStatus === this.storyStates.COMPLETED) { story.completed_date = new Date().toISOString(); if (story.started_date) { const started = new Date(story.started_date); const completed = new Date(story.completed_date); story.actual_hours = (completed - started) / (1000 * 60 * 60); } } // Save status await this.saveStatus(); console.log(chalk.cyan(` āœ… Updated ${storyId}: ${oldStatus} → ${newStatus}`)); // Check for newly unblocked stories if (newStatus === this.storyStates.COMPLETED) { this.checkUnblockedStories(); } return story; } /** * Check for stories that are now unblocked */ checkUnblockedStories() { const unblocked = []; for (const story of this.queue) { if (story.status === this.storyStates.BLOCKED) { if (this.areDependenciesMet(story)) { story.status = this.storyStates.PENDING; unblocked.push(story.id); } } } if (unblocked.length > 0) { console.log(chalk.green(` šŸ”“ Unblocked stories: ${unblocked.join(', ')}`)); } } /** * Get queue status summary */ getQueueStatus() { const summary = { total: this.queue.length, pending: 0, in_progress: 0, in_review: 0, completed: 0, blocked: 0, skipped: 0, completion_percentage: 0, estimated_remaining_hours: 0, actual_completed_hours: 0, }; for (const story of this.queue) { summary[story.status] = (summary[story.status] || 0) + 1; if (story.status === this.storyStates.COMPLETED) { summary.actual_completed_hours += story.actual_hours || 0; } else if (story.status !== this.storyStates.SKIPPED) { summary.estimated_remaining_hours += story.estimated_hours || 0; } } summary.completion_percentage = Math.round((summary.completed / summary.total) * 100); return summary; } /** * Display queue status */ displayQueueStatus() { const status = this.getQueueStatus(); console.log(chalk.blue('\nšŸ“Š Story Queue Status')); console.log(chalk.gray('─'.repeat(40))); console.log(`Total Stories: ${status.total}`); console.log(` ${chalk.gray('ā—‹')} Pending: ${status.pending}`); console.log(` ${chalk.yellow('ā—')} In Progress: ${status.in_progress}`); console.log(` ${chalk.cyan('ā—')} In Review: ${status.in_review}`); console.log(` ${chalk.green('ā—')} Completed: ${status.completed}`); console.log(` ${chalk.red('ā—')} Blocked: ${status.blocked}`); console.log(` ${chalk.gray('ā—')} Skipped: ${status.skipped}`); console.log(chalk.gray('─'.repeat(40))); console.log(`Completion: ${status.completion_percentage}%`); console.log(`Remaining Hours: ${status.estimated_remaining_hours.toFixed(1)}`); console.log(`Completed Hours: ${status.actual_completed_hours.toFixed(1)}`); // Show progress bar const progressBar = this.createProgressBar(status.completion_percentage); console.log(`Progress: ${progressBar}`); // Show current story if (this.currentStory) { console.log(chalk.gray('─'.repeat(40))); console.log(chalk.yellow(`Current: ${this.currentStory.id}`)); console.log(` ${this.currentStory.title}`); } } /** * Create visual progress bar */ createProgressBar(percentage) { const width = 30; const filled = Math.round((percentage / 100) * width); const empty = width - filled; const bar = chalk.green('ā–ˆ'.repeat(filled)) + chalk.gray('ā–‘'.repeat(empty)); return `[${bar}] ${percentage}%`; } /** * Get stories by status */ getStoriesByStatus(status) { return this.queue.filter((story) => story.status === status); } /** * Get blocked stories with reasons */ getBlockedStories() { const blocked = []; for (const story of this.queue) { if ( story.status === this.storyStates.BLOCKED || (story.status === this.storyStates.PENDING && !this.areDependenciesMet(story)) ) { const blockingDeps = []; for (const depId of story.dependencies || []) { const dep = this.queue.find((s) => s.id === depId); if (!dep || dep.status !== this.storyStates.COMPLETED) { blockingDeps.push(depId); } } blocked.push({ story: story, blocking_dependencies: blockingDeps, }); } } return blocked; } /** * Load queue from file */ async loadQueue() { if (await fs.pathExists(this.options.queueFile)) { const content = await fs.readFile(this.options.queueFile, 'utf-8'); this.queue = yaml.load(content) || []; } } /** * Save queue to file */ async saveQueue() { await fs.ensureDir(path.dirname(this.options.queueFile)); await fs.writeFile(this.options.queueFile, yaml.dump(this.queue, { indent: 2 })); } /** * Load status from file */ async loadStatus() { if (await fs.pathExists(this.options.statusFile)) { const content = await fs.readFile(this.options.statusFile, 'utf-8'); this.status = yaml.load(content) || {}; // Sync status with queue for (const story of this.queue) { if (this.status[story.id]) { Object.assign(story, this.status[story.id]); } } } } /** * Save status to file */ async saveStatus() { // Build status object from queue this.status = {}; for (const story of this.queue) { this.status[story.id] = { status: story.status, assigned_to: story.assigned_to, started_date: story.started_date, completed_date: story.completed_date, actual_hours: story.actual_hours, status_history: story.status_history, }; } await fs.ensureDir(path.dirname(this.options.statusFile)); await fs.writeFile(this.options.statusFile, yaml.dump(this.status, { indent: 2 })); } /** * Export queue report */ async exportReport(outputFile) { const report = { generated_date: new Date().toISOString(), summary: this.getQueueStatus(), stories: this.queue, blocked_analysis: this.getBlockedStories(), }; await fs.writeJson(outputFile, report, { spaces: 2 }); console.log(chalk.green(`šŸ“„ Report exported to ${outputFile}`)); } } // CLI Interface if (require.main === module) { const { program } = require('commander'); program.description('Manage story queue for Salesforce development'); program .command('init') .description('Initialize story queue from story files') .option('-d, --dir <directory>', 'Story directory', 'docs/stories') .action(async (options) => { const manager = new StoryQueueManager({ storyDir: options.dir }); await manager.initializeQueue(); manager.displayQueueStatus(); }); program .command('next') .description('Get next story from queue') .action(async () => { const manager = new StoryQueueManager(); await manager.initializeQueue(); const story = await manager.getNextStory(); if (story) { console.log(chalk.blue(`\nšŸ“‚ Story file: ${story.file}`)); } }); program .command('update <storyId> <status>') .description('Update story status') .option('-n, --notes <notes>', 'Status change notes') .action(async (storyId, status, options) => { const manager = new StoryQueueManager(); await manager.initializeQueue(); await manager.updateStoryStatus(storyId, status, options.notes || ''); manager.displayQueueStatus(); }); program .command('status') .description('Display queue status') .action(async () => { const manager = new StoryQueueManager(); await manager.initializeQueue(); manager.displayQueueStatus(); }); program .command('blocked') .description('Show blocked stories') .action(async () => { const manager = new StoryQueueManager(); await manager.initializeQueue(); const blocked = manager.getBlockedStories(); console.log(chalk.red(`\n🚫 Blocked Stories: ${blocked.length}`)); for (const item of blocked) { console.log(`\n${item.story.id}: ${item.story.title}`); console.log(` Blocked by: ${item.blocking_dependencies.join(', ')}`); } }); program .command('report <output>') .description('Export queue report') .action(async (output) => { const manager = new StoryQueueManager(); await manager.initializeQueue(); await manager.exportReport(output); }); program.parse(); } module.exports = StoryQueueManager;