UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

197 lines (167 loc) 6.19 kB
import { EventEmitter } from 'events'; import { ProjectMethodology, MethodologyStore, MethodologyMigration, AGILE_TOOLS, KANBAN_TOOLS, COMMON_TOOLS } from './types.js'; import { ConfigManager } from '../../config/config-manager.js'; import { randomUUID } from 'crypto'; export class MethodologyManager extends EventEmitter { private store: MethodologyStore; private configManager: ConfigManager; constructor(configManager?: ConfigManager) { super(); this.configManager = configManager || new ConfigManager(); this.store = {}; } async init(): Promise<void> { await this.loadStore(); } async setMethodology(type: 'agile' | 'kanban', userId: string): Promise<ProjectMethodology> { if (this.store.currentMethodology?.lockedAt) { throw new Error(`Methodology is locked since ${this.store.currentMethodology.lockedAt}. Cannot change.`); } const methodology: ProjectMethodology = { type, allowedTools: [...COMMON_TOOLS, ...(type === 'agile' ? AGILE_TOOLS : KANBAN_TOOLS)], disallowedTools: type === 'agile' ? KANBAN_TOOLS : AGILE_TOOLS, configuration: this.getDefaultConfig(type), migrationHistory: [] }; if (this.store.currentMethodology && this.store.currentMethodology.type !== type) { // Record migration const migration: MethodologyMigration = { id: randomUUID(), fromType: this.store.currentMethodology.type, toType: type, migratedAt: new Date(), migratedBy: userId, itemsMigrated: 0, dataLost: this.identifyDataLoss(this.store.currentMethodology.type, type) }; methodology.migrationHistory.push(migration); } this.store.currentMethodology = methodology; await this.saveStore(); this.emit('methodology-changed', methodology); return methodology; } async lockMethodology(userId: string): Promise<void> { if (!this.store.currentMethodology) { throw new Error('No methodology set to lock'); } if (this.store.currentMethodology.lockedAt) { throw new Error('Methodology is already locked'); } this.store.currentMethodology.lockedAt = new Date(); this.store.currentMethodology.lockedBy = userId; await this.saveStore(); this.emit('methodology-locked', this.store.currentMethodology); } isToolAllowed(toolName: string): boolean { if (!this.store.currentMethodology) { return true; // No methodology set, allow all } return this.store.currentMethodology.allowedTools.includes(toolName); } getCurrentMethodology(): ProjectMethodology | undefined { return this.store.currentMethodology; } async warnAboutConflict(toolName: string): Promise<boolean> { const now = new Date(); const lastWarning = this.store.lastWarningShown; // Only show warning once per hour if (lastWarning && (now.getTime() - lastWarning.getTime()) < 3600000) { return false; } this.store.lastWarningShown = now; await this.saveStore(); this.emit('methodology-conflict-warning', { toolName, currentMethodology: this.store.currentMethodology?.type, message: `Tool '${toolName}' belongs to a different methodology. Consider switching methodologies or using the appropriate tools.` }); return true; } private getDefaultConfig(type: 'agile' | 'kanban'): any { if (type === 'agile') { return { sprintDuration: 14, velocityTracking: true, storyPointScale: [1, 2, 3, 5, 8, 13, 21], retrospectiveFrequency: 'end_of_sprint', customColumns: ['To Do', 'In Progress', 'Review', 'Testing', 'Done'], defaultPriorities: ['low', 'medium', 'high', 'critical'], requiredFields: ['title', 'description', 'storyPoints', 'acceptanceCriteria'] }; } else { return { wip_limits: { 'To Do': 0, 'In Progress': 3, 'Review': 2, 'Done': 0 }, cycleTimeTracking: true, leadTimeTracking: true, customColumns: ['Backlog', 'To Do', 'In Progress', 'Review', 'Done'], defaultPriorities: ['low', 'medium', 'high', 'urgent'], requiredFields: ['title', 'description'] }; } } private identifyDataLoss(from: string, to: string): string[] { const dataLoss: string[] = []; if (from === 'agile' && to === 'kanban') { dataLoss.push( 'Sprint information will be lost', 'Story points will not be used in Kanban', 'Velocity tracking will be disabled', 'Epic relationships may need adjustment' ); } else if (from === 'kanban' && to === 'agile') { dataLoss.push( 'WIP limits will be removed', 'Cycle time tracking will be converted to velocity', 'Tasks will need story point estimation', 'Tasks will need to be assigned to sprints' ); } return dataLoss; } async suggestMigrationPath(from: string, to: string): Promise<string[]> { const suggestions: string[] = []; if (from === 'agile' && to === 'kanban') { suggestions.push( '1. Complete or cancel active sprints', '2. Convert stories to tasks', '3. Set up WIP limits for columns', '4. Archive velocity reports', '5. Train team on Kanban metrics' ); } else if (from === 'kanban' && to === 'agile') { suggestions.push( '1. Group related tasks into stories', '2. Estimate story points for all items', '3. Create initial sprint backlog', '4. Define sprint cadence', '5. Set up velocity tracking' ); } return suggestions; } private async loadStore(): Promise<void> { const storageManager = this.configManager.getStorageManager(); const data = await storageManager.loadData('methodology-config', 'methodology.json'); if (data) { this.store = data; } } private async saveStore(): Promise<void> { const storageManager = this.configManager.getStorageManager(); await storageManager.saveData('methodology-config', 'methodology.json', this.store); } }