Skip to main content
Glama
development.js9.34 kB
const Project = require('../db/models/Project'); const WorkPackage = require('../db/models/WorkPackage'); const Task = require('../db/models/Task'); const FileRegistry = require('../db/models/FileRegistry'); const ProjectState = require('../db/models/ProjectState'); const orchestratorService = require('./orchestrator'); class DevelopmentService { /** * Start working on a task * @param {String} taskId - Task ID * @returns {Object} Updated task and project state */ async startTask(taskId) { const task = await Task.findByPk(taskId, { include: [{ model: WorkPackage, include: [Project] }] }); if (!task) { throw new Error('Task not found'); } // Check if task is in PLANNED state if (task.status !== 'PLANNED' && task.status !== 'FAILED') { throw new Error(`Task is already in ${task.status} state`); } // Check if all dependencies are completed if (task.dependencies && task.dependencies.length > 0) { const dependencyTasks = await Task.findAll({ where: { taskId: task.dependencies } }); const incompleteDeps = dependencyTasks.filter(dt => dt.status !== 'COMPLETED'); if (incompleteDeps.length > 0) { throw new Error(`Cannot start task: ${incompleteDeps.length} dependencies are not completed`); } } // Update task status task.status = 'IN_PROGRESS'; await task.save(); // Update work package status if it's the first task const workPackage = task.WorkPackage; if (workPackage.status === 'PLANNED') { workPackage.status = 'IN_PROGRESS'; await workPackage.save(); } // Create or update file registry entry let fileRecord = await FileRegistry.findOne({ where: { projectId: workPackage.Project.id, filePath: task.filePath } }); if (!fileRecord) { fileRecord = await FileRegistry.create({ projectId: workPackage.Project.id, filePath: task.filePath, modificationHistory: [], lastModifiedBy: task.id }); } // Update project state const projectId = workPackage.Project.id; const currentState = await orchestratorService.getCurrentState(projectId); const updatedState = { ...currentState, activeTask: { id: task.id, taskId: task.taskId, name: task.name, filePath: task.filePath } }; await ProjectState.create({ projectId, state: updatedState }); return { task, state: updatedState }; } /** * Complete a development task and mark ready for QA * @param {String} taskId - Task ID * @param {Object} implementationData - Implementation details * @returns {Object} Updated task and project state */ async completeTask(taskId, implementationData) { const task = await Task.findByPk(taskId, { include: [{ model: WorkPackage, include: [Project] }] }); if (!task) { throw new Error('Task not found'); } // Ensure task is in progress if (task.status !== 'IN_PROGRESS') { throw new Error(`Task must be IN_PROGRESS to complete. Current status: ${task.status}`); } // Update task with implementation details task.changes = implementationData.changes || task.changes; task.status = 'READY_FOR_QA'; await task.save(); // Update file registry const fileRecord = await FileRegistry.findOne({ where: { projectId: task.WorkPackage.Project.id, filePath: task.filePath } }); if (fileRecord) { const modHistory = fileRecord.modificationHistory || []; modHistory.push({ taskId: task.taskId, timestamp: new Date().toISOString(), changes: implementationData.changes }); fileRecord.modificationHistory = modHistory; fileRecord.currentState = implementationData.fileState || fileRecord.currentState; fileRecord.lastModifiedBy = task.id; await fileRecord.save(); } // Update project state const projectId = task.WorkPackage.Project.id; const currentState = await orchestratorService.getCurrentState(projectId); // Find other tasks pending QA const pendingQaTasks = await Task.findAll({ where: { status: 'READY_FOR_QA' }, include: [{ model: WorkPackage, where: { projectId } }] }); const updatedState = { ...currentState, activeTask: null, pendingQA: pendingQaTasks.map(t => ({ id: t.id, taskId: t.taskId, name: t.name })) }; await ProjectState.create({ projectId, state: updatedState }); // If we have pending QA tasks, transition to QA role if (pendingQaTasks.length === 1) { // First task ready for QA return orchestratorService.transitionRole(projectId, 'QA', { message: 'Task ready for QA review', pendingQA: pendingQaTasks.map(t => t.taskId) }); } return { task, state: updatedState }; } /** * Get the next task to work on * @param {String} projectId - Project ID * @returns {Object} Next task information or null if none */ async getNextTask(projectId) { // Use orchestrator service to get next pending task const nextTask = await orchestratorService.getNextPendingTask(projectId); if (!nextTask) { const project = await Project.findByPk(projectId); // Check if all tasks are completed or in QA const allTasks = await Task.findAll({ include: [{ model: WorkPackage, where: { projectId } }] }); const allTasksComplete = allTasks.every( t => t.status === 'COMPLETED' || t.status === 'READY_FOR_QA' || t.status === 'QA_IN_PROGRESS' ); if (allTasksComplete) { // All tasks are either complete or in QA return { message: 'All tasks are either completed or in QA', nextStep: 'Wait for QA completion' }; } // Some tasks might be blocked by dependencies return { message: 'No available tasks to work on. Some tasks might be blocked by dependencies.', blockedTasks: allTasks .filter(t => t.status === 'PLANNED' && t.dependencies.length > 0) .map(t => t.taskId) }; } return { task: nextTask, message: `Ready to start task ${nextTask.taskId}: ${nextTask.name}` }; } /** * Record implementation checkpoint * @param {String} taskId - Task ID * @param {Object} checkpointData - Current implementation state * @returns {Object} Updated project state */ async saveImplementationCheckpoint(taskId, checkpointData) { const task = await Task.findByPk(taskId, { include: [{ model: WorkPackage, include: [Project] }] }); if (!task) { throw new Error('Task not found'); } // Ensure task is in progress if (task.status !== 'IN_PROGRESS') { throw new Error(`Task must be IN_PROGRESS to save checkpoint. Current status: ${task.status}`); } const projectId = task.WorkPackage.Project.id; // Update file registry with current state if (checkpointData.fileState) { let fileRecord = await FileRegistry.findOne({ where: { projectId, filePath: task.filePath } }); if (fileRecord) { fileRecord.currentState = checkpointData.fileState; await fileRecord.save(); } } // Create checkpoint state const checkpointState = await orchestratorService.saveCheckpoint( projectId, { taskId: task.taskId, implementationState: checkpointData } ); return checkpointState; } /** * Resume work on a task after interruption * @param {String} taskId - Task ID * @returns {Object} Task context with file state */ async resumeTask(taskId) { const task = await Task.findByPk(taskId, { include: [{ model: WorkPackage, include: [Project] }] }); if (!task) { throw new Error('Task not found'); } // Get file state from registry const fileRecord = await FileRegistry.findOne({ where: { projectId: task.WorkPackage.Project.id, filePath: task.filePath } }); // Get latest checkpoint for this task const projectId = task.WorkPackage.Project.id; const latestState = await ProjectState.findOne({ where: { projectId, checkpoint: true }, order: [['timestamp', 'DESC']] }); // Extract task-specific context from checkpoint if available let implementationContext = {}; if (latestState && latestState.state.checkpoint && latestState.state.checkpoint.data.taskId === task.taskId) { implementationContext = latestState.state.checkpoint.data.implementationState || {}; } return { task, fileState: fileRecord ? fileRecord.currentState : {}, fileHistory: fileRecord ? fileRecord.modificationHistory : [], implementationContext, workPackage: task.WorkPackage }; } } module.exports = new DevelopmentService();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docherty/contextmgr-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server