@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
197 lines (167 loc) • 6.19 kB
text/typescript
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);
}
}