UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

227 lines (194 loc) 6.95 kB
// File: common/utils/clickup-helpers.js /** * ClickUp Helper Functions - Provides abstraction over ClickUp MCP tool * * This module provides utilities for: * - Updating story-status custom field (for Stories) * - Updating native status field (for Epics) * - Updating task descriptions with markdown * - Adding changelog comments to tasks * * CRITICAL DISTINCTION: * - Stories: Use custom field "story-status" * - Epics: Use native ClickUp status field */ const { mapStatusToClickUp } = require('./status-mapper'); /** * Resolves the ClickUp MCP tool * Tries tool-resolver first (for tests), falls back to global references */ async function getClickUpTool() { try { // Try using tool-resolver (test environment or future production) const { resolveTool } = require('./tool-resolver'); return await resolveTool('clickup'); } catch (error) { // Fall back to global references (current production pattern) return { updateTask: global.mcp__clickup__update_task, createComment: global.mcp__clickup__create_task_comment, getTask: global.mcp__clickup__get_task, }; } } /** * Updates story-status custom field in ClickUp * * CRITICAL: Stories use custom field "story-status", NOT native status * * @param {string} taskId - ClickUp task ID (can be regular or custom ID) * @param {string} newStatus - New AIOS status value (will be mapped to ClickUp) * @returns {Promise<void>} * @throws {Error} If ClickUp API call fails */ async function updateStoryStatus(taskId, newStatus) { try { // Map AIOS status to ClickUp story-status value const mappedStatus = mapStatusToClickUp(newStatus); console.log(`Updating story ${taskId} status to: ${mappedStatus}`); // Note: This assumes the ClickUp MCP tool is available // Refer to tools/mcp/clickup.yaml for update_task with custom_fields const tool = await getClickUpTool(); await tool.updateTask({ taskId: taskId, custom_fields: [ { id: 'story-status', value: mappedStatus, }, ], }); console.log('✅ Story status updated successfully'); } catch (error) { console.error(`Error updating story status for ${taskId}:`, error); throw new Error(`Failed to update story status: ${error.message}`); } } /** * Updates Epic status using native ClickUp status field * * CRITICAL: Epics use native status field, NOT custom field * * @param {string} epicTaskId - Epic task ID * @param {string} newStatus - One of: "Planning", "In Progress", "Done" * @returns {Promise<void>} * @throws {Error} If ClickUp API call fails */ async function updateEpicStatus(epicTaskId, newStatus) { try { // Validate Epic status (must be one of the three valid values) const validStatuses = ['Planning', 'In Progress', 'Done']; if (!validStatuses.includes(newStatus)) { throw new Error(`Invalid Epic status: ${newStatus}. Must be one of: ${validStatuses.join(', ')}`); } console.log(`Updating Epic ${epicTaskId} status to: ${newStatus}`); // Use native status field (not custom_fields) const tool = await getClickUpTool(); await tool.updateTask({ taskId: epicTaskId, status: newStatus, // Native field for Epics }); console.log('✅ Epic status updated successfully'); } catch (error) { console.error(`Error updating Epic status for ${epicTaskId}:`, error); throw new Error(`Failed to update Epic status: ${error.message}`); } } /** * Updates ClickUp task description with full story markdown * * @param {string} taskId - ClickUp task ID * @param {string} markdown - Full story markdown content * @returns {Promise<void>} * @throws {Error} If ClickUp API call fails */ async function updateTaskDescription(taskId, markdown) { try { console.log(`Updating task ${taskId} description (${markdown.length} chars)`); const tool = await getClickUpTool(); await tool.updateTask({ taskId: taskId, markdown_description: markdown, }); console.log('✅ Task description updated successfully'); } catch (error) { console.error(`Error updating task description for ${taskId}:`, error); throw new Error(`Failed to update task description: ${error.message}`); } } /** * Adds a changelog comment to a ClickUp task * * @param {string} taskId - ClickUp task ID * @param {string} comment - Changelog markdown content * @returns {Promise<void>} * @throws {Error} If ClickUp API call fails */ async function addTaskComment(taskId, comment) { try { console.log(`Adding changelog comment to task ${taskId}`); const tool = await getClickUpTool(); await tool.createComment({ taskId: taskId, commentText: comment, }); console.log('✅ Changelog comment added successfully'); } catch (error) { console.error(`Error adding comment to task ${taskId}:`, error); throw new Error(`Failed to add task comment: ${error.message}`); } } /** * Verifies that an Epic task exists in ClickUp Backlog list * * Implements AC1: Epic Verification * Searches for Epic by epic number using tags and validates status * * @param {number} epicNum - Epic number to verify * @returns {Promise<object>} Epic verification result: { found, epicTaskId, epic } * @throws {Error} If Epic doesn't exist or has invalid status */ async function verifyEpicExists(epicNum) { try { console.log(`Verifying Epic ${epicNum} exists...`); const tool = await getClickUpTool(); // Search for Epic by tag in workspace // Using getWorkspaceTasks to find Epic with specific tag const result = await tool.getWorkspaceTasks({ tags: [`epic-${epicNum}`], }); if (!result || !result.tasks || result.tasks.length === 0) { throw new Error(`Epic ${epicNum} not found in ClickUp Backlog list. Please create Epic task with tags: ['epic', 'epic-${epicNum}'] and status: Planning or In Progress`); } // Filter for Epics (should have 'epic' tag and valid status) const epics = result.tasks.filter(task => task.tags && task.tags.includes('epic') && task.tags.includes(`epic-${epicNum}`) && ['Planning', 'In Progress'].includes(task.status), ); if (epics.length === 0) { throw new Error(`Epic ${epicNum} found but has invalid status. Status must be 'Planning' or 'In Progress'.`); } if (epics.length > 1) { console.warn(`⚠️ Multiple Epics found with epic-${epicNum} tag. Using first one: ${epics[0].id}`); } const epic = epics[0]; console.log(`✅ Epic verified: ${epic.name || epic.id}`); return { found: true, epicTaskId: epic.id, epic: epic, }; } catch (error) { console.error(`Error verifying Epic ${epicNum}:`, error); throw error; } } module.exports = { updateStoryStatus, updateEpicStatus, updateTaskDescription, addTaskComment, verifyEpicExists, };