UNPKG

conductor-tasks

Version:

Task Manager for AI Development

943 lines (914 loc) 139 kB
import { TaskPriority, TaskStatus, ContextPriority, CodeSymbolType } from '../core/types.js'; import * as fs from 'fs'; import * as path from 'path'; import ts from 'typescript'; import { errorHandler, ErrorCategory, ErrorSeverity, TaskError } from '../core/errorHandler.js'; import markdownRenderer from '../core/markdownRenderer.js'; import { ErrorHandler } from '../core/errorHandler.js'; import logger from '../core/logger.js'; import { JsonUtils } from '../core/jsonUtils.js'; const DEFAULT_CONFIG = { defaultSubtasks: 3, defaultPriority: TaskPriority.MEDIUM, tasksFileName: 'TASKS.md' }; function detectWorkspaceDirectory() { if (process.env.WORKSPACE_FOLDER_PATHS) { const paths = process.env.WORKSPACE_FOLDER_PATHS.split(';'); if (paths.length > 0 && paths[0]) { logger.info(`Using workspace path from WORKSPACE_FOLDER_PATHS: ${paths[0]}`); return paths[0]; } } const cwd = process.cwd(); logger.info(`No workspace path found, using current directory: ${cwd}`); return cwd; } export class TaskManager { constructor(llmManager, contextManager, tasksFilePath) { this.tasks = new Map(); this.tasksFilePath = ''; this.initialized = false; this.llmManager = llmManager; this.contextManager = contextManager; this.workspaceRoot = detectWorkspaceDirectory(); logger.info(`Using workspace root: ${this.workspaceRoot}`); try { process.chdir(this.workspaceRoot); logger.info(`Changed process.cwd() to workspace root: ${process.cwd()}`); } catch (err) { logger.warn(`Failed to change process.cwd() to workspace root: ${err}`); } this.config = { defaultSubtasks: this.getEnvInt('DEFAULT_SUBTASKS', DEFAULT_CONFIG.defaultSubtasks), defaultPriority: this.getEnvPriority('DEFAULT_PRIORITY', DEFAULT_CONFIG.defaultPriority), tasksFileName: process.env.TASKS_FILENAME || DEFAULT_CONFIG.tasksFileName }; if (tasksFilePath) { this.setTasksFilePath(tasksFilePath); } else { this.tasksFilePath = path.join(this.workspaceRoot, this.config.tasksFileName); } logger.info(`Tasks file path set to: ${this.tasksFilePath}`); if (fs.existsSync(this.tasksFilePath)) { try { this.loadTasks(); this.initialized = true; logger.info(`Successfully loaded tasks from ${this.tasksFilePath}`); } catch (error) { logger.error(`Failed to load tasks from ${this.tasksFilePath}`, { error }); } } } getEnvInt(key, defaultValue) { return process.env[key] ? parseInt(process.env[key], 10) : defaultValue; } getEnvPriority(key, defaultValue) { if (!process.env[key]) return defaultValue; const value = process.env[key].toLowerCase(); switch (value) { case 'critical': return TaskPriority.CRITICAL; case 'high': return TaskPriority.HIGH; case 'medium': return TaskPriority.MEDIUM; case 'low': return TaskPriority.LOW; case 'backlog': return TaskPriority.BACKLOG; default: return defaultValue; } } async initialize(projectName, projectDescription, filePath) { logger.info(`TaskManager.initialize called. Current workspace root: ${this.workspaceRoot}. Initializing with tasks file: ${filePath}`); const normalizedPath = this.normalizePath(filePath); logger.info(`Normalized file path for initialization: ${normalizedPath}`); if (fs.existsSync(normalizedPath)) { logger.warn(`Task file already exists at ${normalizedPath}. Initialization via TaskManager.initialize will not proceed if it implies overwriting.`); throw new Error(`Task file already exists at ${normalizedPath}. Cannot initialize a new task system at this location.`); } this.setTasksFilePath(normalizedPath); logger.info(`Tasks file path set during initialization: ${this.tasksFilePath}`); this.contextManager.setProjectContext(`Project: ${projectName}\nDescription: ${projectDescription}`); await this.createEnhancedInitialTasksWithLLM(projectName, projectDescription); this.saveTasks(); this.initialized = true; logger.info(`Task manager successfully initialized for project: ${projectName}`); } createDefaultTasksTemplate(projectName, projectDescription) { const now = new Date().toLocaleDateString(); const setupTaskId = `task-${Date.now()}-setup`; const setupTask = { id: setupTaskId, title: `Set up ${projectName}`, description: `Initial setup and configuration for the ${projectName} project.\n\n${projectDescription}`, priority: TaskPriority.HIGH, status: TaskStatus.TODO, complexity: 3, createdAt: Date.now(), updatedAt: Date.now(), dependencies: [], tags: ['setup', 'initialization'], notes: [ { id: `note-${Date.now()}`, content: `Project initialized on ${now}`, timestamp: Date.now(), author: 'System', type: 'comment' } ] }; this.tasks.set(setupTaskId, setupTask); logger.info(`Created default template with setup task: ${setupTaskId}`); } async createEnhancedInitialTasksWithLLM(projectName, projectDescription) { try { this.createDefaultTasksTemplate(projectName, projectDescription); if (!this.llmManager.sendRequest) { logger.info('LLM not available, skipping enhanced initialization'); return; } let codebaseContext = 'Codebase context analysis not available or failed.'; try { const structuredAnalysis = await this.getStructuredCodebaseAnalysis(projectName, projectDescription); codebaseContext = this._getCodebaseContextString(structuredAnalysis); } catch (error) { logger.warn('Failed to analyze codebase for implementation steps', error); } const prompt = ` You are an expert Technical Project Manager and System Architect. Your role is to define a set of specific, actionable, and high-quality initial tasks for a prompt engineering tool project based on its description and available codebase context. The goal is to produce a robust foundational task list. # PROJECT DETAILS: Project Name: ${projectName} Project Description: ${projectDescription} # AVAILABLE CODEBASE CONTEXT SUMMARY (High-Level): ${codebaseContext ? codebaseContext : "No specific codebase context provided. Assume a new project or infer from description. Focus on establishing core functionalities."} # INSTRUCTIONS: 1. **Analyze & Strategize**: Thoroughly review the project name, description, and codebase context. Identify the most critical functionalities and foundational elements needed for success. * Pay special attention to any README or PLAN content that outlines project goals, design, or architecture. * Consider existing project structure including directories, key files, and current functionality to identify what's already in place and what's missing. * Note any specific technologies, frameworks, or patterns already in use, and leverage them in task definitions. 2. **Propose High-Quality Initial Tasks**: Define 5-7 well-scoped, specific, and actionable tasks. These tasks should establish a strong foundation or tackle the most critical aspects first. * Tasks MUST directly relate to the actual project description and structure found in the codebase context (if available). * Tasks should logically build on what already exists, or outline the creation of essential new components. * Avoid overly broad tasks (e.g., "Develop backend"). Instead, break them down into more concrete deliverables (e.g., "Design and implement API for user authentication"). * Ensure tasks are not too granular for an initial set; they should represent significant pieces of work. 3. **Task Focus Areas** - Based on the actual code/files detected and README/PLAN content, or core requirements for a new prompt engineering tool: * **Core Prompt Engineering Features**: E.g., "Implement robust prompt templating engine with variable substitution", "Develop prompt versioning and history tracking". * **LLM Provider Integration**: E.g., "Abstract LLM provider interface and implement OpenAI connector", "Implement secure API key management for LLM providers". * **IDE/Workspace Integration**: E.g., "Develop service for workspace-aware code context retrieval", "Design mechanism for injecting context into active editor". * **User Interface/API (if applicable)**: E.g., "Design and implement core API for prompt management (CRUD operations)", "Develop basic UI for creating and testing prompts". * **Essential Tooling/Setup**: E.g., "Establish comprehensive logging and error handling framework", "Set up automated testing pipeline for core modules". (Only if not present and clearly essential). 4. **Leverage Context & Be Specific** (CRITICAL): * **Reference specific files and directories** that are mentioned in the codebase context if they are relevant to the new task. * If README.md or PLAN.md content is included, ensure tasks align with their stated goals and plans. * Create tasks that build upon the existing structure or propose clear new structures. E.g., "Extend \`src/llm/providers\` to support Anthropic Claude API". * For new projects, tasks should define the creation of these key structures. 5. **Format Output**: For each proposed task, provide the required information in the exact format specified below. Ensure all fields are thoughtfully completed. # REQUIRED TASK FIELDS (Provide all for each task, ensuring high quality and detail): 1. **TITLE**: Concise, action-oriented, and specific title (e.g., "Implement Secure API Key Storage using Vault"). 2. **DESCRIPTION**: Detailed explanation (3-5 sentences) of the task's purpose, scope, key deliverables, and its importance to the project. If it builds on existing code, mention how. If it creates something new, describe what and why. Reference actual files/folders from the codebase context where applicable. 3. **PRIORITY**: Choose one: critical, high, medium. Base this on urgency and foundational impact for a new project. 4. **COMPLEXITY**: Integer rating from 1 (very simple) to 10 (very complex), reflecting estimated effort. 5. **TAGS**: Comma-separated list of 3-5 highly relevant keywords (e.g., \`frontend\`, \`authentication\`, \`api\`, \`database\`, \`backend\`, \`security\`, \`server\`). 6. **SUBTASKS**: List 2-4 concrete, actionable first steps or sub-components required to complete this task. Each subtask needs a clear title and a brief (1-sentence) description of what needs to be done and its expected outcome. * *Example Subtask Format:* \`- Define API key encryption strategy: Research and select suitable encryption algorithms and key management practices.\` # IMPORTANT NOTES: - Ensure tasks specifically relate to prompt engineering, LLM integration, IDE integration, or foundational elements for such a tool. - Tasks should directly build on any detected directories and files or propose creation of core new ones. - Focus on concrete implementations rather than vague design tasks. Titles and descriptions must be specific. - Emphasize high-quality, well-defined tasks that a developer can pick up and begin working on. - If suggesting a task for an existing file/directory, be specific about what needs to be *added* or *changed*. `; try { const result = await this.llmManager.sendRequest({ prompt, taskName: "initialize-project" // Add taskName for provider routing }); let parsedTasks; try { parsedTasks = this.parseTasksFromLLMInit(result.text); } catch (parseError) { logger.error('Failed to parse LLM output for initialization', parseError); return; } if (parsedTasks.length > 0) { this.tasks.clear(); logger.info(`Cleared default tasks to replace with ${parsedTasks.length} LLM-generated tasks`); } else { logger.info('No tasks parsed from LLM output, keeping default tasks'); return; } for (const taskData of parsedTasks) { try { const taskId = await this.createTask(taskData.title, taskData.description); const task = this.tasks.get(taskId); if (task) { task.priority = this.stringToPriority(taskData.priority); task.complexity = Math.min(Math.max(1, taskData.complexity), 10); task.tags = taskData.tags || []; this.tasks.set(taskId, task); if (taskData.subtasks && taskData.subtasks.length > 0) { task.subtasks = []; for (const subtaskData of taskData.subtasks) { try { const subtaskId = await this.createSubtask(taskId, subtaskData.title, subtaskData.description); if (subtaskId) { logger.info(`Created subtask ${subtaskId} for task ${taskId}`); } } catch (subtaskError) { logger.warn(`Failed to create subtask for task ${taskId}`, subtaskError); } } } } } catch (taskCreateError) { logger.warn(`Failed to create task from LLM output`, taskCreateError); } } } catch (llmError) { logger.error('Failed to get LLM response for initialization', llmError); } } catch (error) { logger.error('Error during enhanced task initialization', error); if (this.tasks.size === 0) { this.createDefaultTasksTemplate(projectName, projectDescription); } } } parseTasksFromLLMInit(llmOutput) { const tasks = []; try { const taskBlocks = llmOutput.includes('---') ? llmOutput.split('---').filter(block => block.trim().length > 0) : llmOutput.split(/\nTITLE: |\nTask \d+:/).filter((block, index) => index === 0 ? block.toLowerCase().includes('title:') : block.trim().length > 0); if (taskBlocks.length === 0) { logger.warn('No task blocks found in LLM output'); return tasks; } for (const block of taskBlocks) { try { const trimmedBlock = block.trim(); if (trimmedBlock.length === 0) { continue; } const titleMatch = trimmedBlock.match(/TITLE: ?(.*?)(?:\n|$)/i) || trimmedBlock.match(/Task \d+: ?(.*?)(?:\n|$)/i); const descriptionMatch = trimmedBlock.match(/DESCRIPTION: ?(.*?)(?:\n[A-Z]+:|\nSUBTASKS:|\n---|\n\n|$)/is); const priorityMatch = trimmedBlock.match(/PRIORITY: ?(.*?)(?:\n|$)/i); const complexityMatch = trimmedBlock.match(/COMPLEXITY: ?(\d+)/i); const tagsMatch = trimmedBlock.match(/TAGS: ?(.*?)(?:\n|$)/i); if (!titleMatch || !titleMatch[1]) { logger.warn(`Skipping task block without title: ${trimmedBlock.substring(0, 50)}...`); continue; } const title = titleMatch[1].trim(); const description = descriptionMatch && descriptionMatch[1] ? descriptionMatch[1].trim() : 'No description provided'; const priority = priorityMatch && priorityMatch[1] ? priorityMatch[1].trim().toLowerCase() : 'medium'; let complexity = 5; if (complexityMatch && complexityMatch[1]) { try { complexity = parseInt(complexityMatch[1], 10); if (isNaN(complexity) || complexity < 1 || complexity > 10) { complexity = 5; } } catch (complexityError) { logger.warn(`Could not parse complexity, using default (5): ${complexityMatch[1]}`); } } const tags = []; if (tagsMatch && tagsMatch[1] && tagsMatch[1].trim() !== '') { tagsMatch[1].split(',').forEach(tag => { const trimmedTag = tag.trim(); if (trimmedTag && trimmedTag !== '') { tags.push(trimmedTag); } }); } const subtasks = []; const subtasksSection = trimmedBlock.match(/SUBTASKS:\s*([\s\S]*?)(?:\n---|\n\n|$)/i); if (subtasksSection && subtasksSection[1]) { const subtaskLines = subtasksSection[1].split('\n') .filter(line => line.trim().startsWith('-')) .map(line => line.trim().substring(1).trim()); for (const line of subtaskLines) { if (!line || line.trim() === '') continue; const subtaskMatch = line.match(/(.*?): (.*)/); if (subtaskMatch) { subtasks.push({ title: subtaskMatch[1].trim(), description: subtaskMatch[2].trim() }); } else if (line.length > 0) { subtasks.push({ title: line, description: 'No description provided' }); } } } logger.info(`Successfully parsed task: ${title}`); tasks.push({ title, description, priority, complexity, tags, subtasks }); } catch (error) { logger.warn(`Failed to parse task block: ${error instanceof Error ? error.message : String(error)}`); } } return tasks; } catch (error) { logger.error(`Failed to parse tasks from LLM output: ${error instanceof Error ? error.message : String(error)}`); throw error; } } stringToPriority(priority) { const p = priority.toLowerCase(); if (p.includes('critical')) return TaskPriority.CRITICAL; if (p.includes('high')) return TaskPriority.HIGH; if (p.includes('medium')) return TaskPriority.MEDIUM; if (p.includes('low')) return TaskPriority.LOW; if (p.includes('backlog')) return TaskPriority.BACKLOG; return TaskPriority.MEDIUM; } async createTask(title, description, userInput, parentId) { if (!this.initialized && this.tasks.size === 0) { this.workspaceRoot = detectWorkspaceDirectory(); logger.info(`Re-detected workspace root: ${this.workspaceRoot}`); if (!this.tasksFilePath || this.tasksFilePath === path.join(process.cwd(), this.config.tasksFileName)) { this.tasksFilePath = path.join(this.workspaceRoot, this.config.tasksFileName); logger.info(`Updated tasks file path to: ${this.tasksFilePath}`); } } const taskId = `task-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const now = Date.now(); const task = { id: taskId, title, description, priority: TaskPriority.MEDIUM, status: TaskStatus.TODO, complexity: 5, createdAt: now, updatedAt: now, dependencies: [], tags: [], notes: [], subtasks: [], parent: parentId }; if (parentId) { const parentTask = this.tasks.get(parentId); if (parentTask) { if (!parentTask.subtasks) { parentTask.subtasks = []; } parentTask.subtasks.push(taskId); this.tasks.set(parentId, parentTask); } } await this.enhanceTaskWithLLM(task, userInput); this.tasks.set(taskId, task); this.saveTasks(); this.contextManager.addContext(`Task ${task.id}: ${task.title}\nDescription: ${task.description}\nPriority: ${task.priority}\nComplexity: ${task.complexity}`, this.mapTaskPriorityToContextPriority(task.priority), 'task-manager', ['task', task.id, ...task.tags]); return taskId; } mapTaskPriorityToContextPriority(priority) { switch (priority) { case TaskPriority.CRITICAL: return ContextPriority.CRITICAL; case TaskPriority.HIGH: return ContextPriority.ESSENTIAL; default: return ContextPriority.BACKGROUND; } } async createSubtask(parentId, title, description, userInput) { const parentTask = this.tasks.get(parentId); if (!parentTask) { return undefined; } const subtaskId = `task-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const now = Date.now(); const subtask = { id: subtaskId, title, description, priority: parentTask.priority, status: parentTask.status, complexity: 5, createdAt: now, updatedAt: now, dependencies: [], tags: [], notes: [], subtasks: [], parent: parentId }; parentTask.subtasks = parentTask.subtasks || []; parentTask.subtasks.push(subtaskId); this.tasks.set(parentId, parentTask); this.tasks.set(subtaskId, subtask); this.saveTasks(); return subtaskId; } getSubtasks(taskId) { const task = this.tasks.get(taskId); if (!task || !task.subtasks || task.subtasks.length === 0) { return []; } return task.subtasks .map(subtaskId => this.tasks.get(subtaskId)) .filter((subtask) => subtask !== undefined); } getSubtaskHierarchy(taskId) { const task = this.tasks.get(taskId); if (!task) { return null; } const result = { ...task }; if (task.subtasks && task.subtasks.length > 0) { result.subtaskDetails = task.subtasks.map(subtaskId => this.getSubtaskHierarchy(subtaskId)); } return result; } areAllSubtasksComplete(taskId) { const subtasks = this.getSubtasks(taskId); if (subtasks.length === 0) { return true; } return subtasks.every(subtask => subtask.status === TaskStatus.DONE || this.areAllSubtasksComplete(subtask.id)); } calculateTaskProgress(taskId) { const task = this.tasks.get(taskId); if (!task || !task.subtasks || task.subtasks.length === 0) { return task?.status === TaskStatus.DONE ? 100 : 0; } const subtasks = this.getSubtasks(taskId); if (subtasks.length === 0) { return 0; } const completedSubtasks = subtasks.filter(subtask => subtask.status === TaskStatus.DONE || this.areAllSubtasksComplete(subtask.id)).length; return Math.round((completedSubtasks / subtasks.length) * 100); } updateParentTaskStatus(taskId) { const task = this.tasks.get(taskId); if (!task || !task.parent) { return; } const parentTask = this.tasks.get(task.parent); if (!parentTask) { return; } const areAllComplete = this.areAllSubtasksComplete(parentTask.id); if (areAllComplete && parentTask.status !== TaskStatus.DONE) { parentTask.status = TaskStatus.DONE; parentTask.updatedAt = Date.now(); this.tasks.set(parentTask.id, parentTask); this.saveTasks(); if (parentTask.parent) { this.updateParentTaskStatus(parentTask.id); } } } async enhanceTaskWithLLM(task, userInput) { return ErrorHandler.tryCatch(async () => { if (!this.llmManager.sendRequest) { logger.info(`LLM not available, skipping task enhancement for ${task.id}`); task.priority = task.priority || TaskPriority.MEDIUM; task.complexity = task.complexity || 5; task.tags = task.tags || []; return; } try { logger.info(`Enhancing task ${task.id} with LLM`); const projectContext = this.contextManager.getProjectContext?.() || ''; let codebaseContextString = 'Codebase context analysis not available or failed.'; try { const structuredAnalysis = await this.getStructuredCodebaseAnalysis(); codebaseContextString = this._getCodebaseContextString(structuredAnalysis); } catch (e) { logger.warn(`Codebase analysis failed during task enhancement for ${task.id}:`, e); } const subtasks = this.getSubtasks(task.id); const prompt = ` # ROLE: You are an expert Software Architect and Senior Developer. Your task is to create a detailed, actionable, step-by-step implementation plan for the given software development task. # TASK DETAILS: ## Title: ${task.title} ${task.description ? `## Description:\n${task.description}` : ''} ${task.priority ? `## Priority: ${task.priority}` : ''} ${task.status ? `## Status: ${task.status}` : ''} ${task.tags && task.tags.length > 0 ? `## Tags: ${task.tags.join(', ')}` : ''} ${task.complexity ? `## Complexity: ${task.complexity}` : ''} # TASK RELATIONSHIPS: ${task.parent ? `This is a subtask of: "${this.tasks.get(task.parent)?.title}"\nParent description: ${this.tasks.get(task.parent)?.description}\n\n` : 'This is a top-level task.\n'} ${task.parent ? `Related subtasks:\n${this.getSubtasks(task.parent).map(s => `- ${s.title}: ${s.description.substring(0, 100)}...`).join('\n')}` : 'No sibling subtasks identified.'} ${task.dependencies.length > 0 ? `Direct dependencies:\n${task.dependencies.map(depId => `- ${this.tasks.get(depId)?.title}: ${this.tasks.get(depId)?.description.substring(0, 100)}...`).join('\n')}` : 'No direct dependencies or dependents identified.'} # PROJECT CONTEXT: ${projectContext || 'No specific project context provided.'} # CODEBASE CONTEXT SUMMARY: ${codebaseContextString} # EXISTING SUBTASKS (if any): ${subtasks.length > 0 ? subtasks.map(s => `- ${s.title}: ${s.description ? s.description.substring(0, 150) + '...' : '(No description)'}`).join('\n') : 'None'} # INSTRUCTIONS: First, provide a JSON object containing metadata for the task. Enclose this JSON object in triple backticks with the language specifier "json". The JSON object should have the following optional fields: - "priority": string (one of "critical", "high", "medium", "low", "backlog") - "complexity": number (integer between 1 and 10) - "tags": array of strings - "estimatedEffort": string (e.g., "2 days", "4 hours") - "suggestedSubtasks": array of objects, where each object has "title" (string) and "description" (string). Limit to 3-5 subtasks. Example JSON block: \`\`\`json { "priority": "high", "complexity": 7, "tags": ["refactor", "api", "performance"], "estimatedEffort": "3 days", "suggestedSubtasks": [ { "title": "Define API contract", "description": "Specify request/response formats." }, { "title": "Implement core logic", "description": "Write the main processing functions." } ] } \`\`\` After the JSON block, generate a comprehensive, step-by-step implementation plan based *only* on the information provided above. The plan should be clear enough for another developer to follow. **Output Format for the plan (after the JSON block):** Use Markdown. **MUST include the following sections in the Markdown part:** 1. **## Implementation Plan:** * Provide a numbered list of specific, actionable steps from start to finish. The plan should be practical for a developer to execute. * Break down larger steps into smaller, manageable actions. * If referring to specific files or code sections from the 'CODEBASE CONTEXT SUMMARY', be precise. * Include concise code snippets, pseudocode, or configuration examples where they add significant clarity. * Clearly consider the order of operations and any dependencies between steps. 2. **## Key Considerations:** * Highlight important technical decisions, potential challenges, or architectural points. * Mention any assumptions made. If your suggested metadata (priority, complexity, estimated effort) in the JSON block represents a significant change or might not be immediately obvious, briefly justify your reasoning here. * Suggest specific libraries or tools if relevant and not already implied by the context, explaining the benefit. 3. **## Verification & Testing:** * Describe concrete verification steps for key parts of the implementation. * Suggest specific testing approaches (e.g., unit tests for specific functions, integration tests for API endpoints, manual E2E checks for UI flows) relevant to the plan. Outline example test cases if appropriate. 4. **## Missing Information (Optional):** * If critical information is missing to create a robust plan, clearly state what specific questions need to be answered or what details are required. **Tone:** Professional, clear, and concise. Be comprehensive but avoid unnecessary verbosity. # FINAL CHECKLIST BEFORE RESPONDING: - Does the response start *exactly* with the \`\`\`json block? - Is the JSON block valid and contains the specified fields (priority, complexity, tags, estimatedEffort, suggestedSubtasks)? - Is the JSON block immediately followed by the Markdown plan? - Does the Markdown plan include ALL required sections (Implementation Plan, Key Considerations, Verification & Testing)? - Are the implementation steps concrete, actionable, and reference codebase context where relevant? - Is the response free of any other introductory/concluding text or apologies? `; try { let llmOutput = ''; let metadata = null; let markdownContent = ''; const maxAttempts = 2; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const currentPrompt = (attempt > 1 && task.notes.find(n => n.type === 'comment' && n.author === 'AI_PROMPT_REFINER')?.content) ? task.notes.find(n => n.type === 'comment' && n.author === 'AI_PROMPT_REFINER').content : prompt; const result = await this.llmManager.sendRequest({ prompt: currentPrompt }); llmOutput = result.text.trim(); const jsonBlockRegex = /```json\s*([\s\S]*?)\s*```/; const jsonMatch = llmOutput.match(jsonBlockRegex); if (jsonMatch && jsonMatch[1]) { const jsonString = jsonMatch[1].trim(); try { const parsedJson = JSON.parse(jsonString); if (typeof parsedJson === 'object' && parsedJson !== null && !Array.isArray(parsedJson)) { metadata = parsedJson; markdownContent = llmOutput.substring(jsonMatch[0].length).trim(); logger.info(`Successfully parsed JSON metadata on attempt ${attempt} for task ${task.id}`); break; } else { logger.warn(`Parsed JSON is not a single object on attempt ${attempt} for task ${task.id}: ${jsonString}`); } } catch (parseError) { logger.warn(`Failed to parse extracted JSON on attempt ${attempt} for task ${task.id}: ${jsonString}`, { error: parseError }); } } else { logger.warn(`Could not find JSON block in LLM output on attempt ${attempt} for task ${task.id}: ${llmOutput.substring(0, 200)}...`); } if (!metadata && attempt < maxAttempts) { logger.info(`Attempting to refine prompt for task ${task.id} after failed attempt ${attempt}`); const desiredSpec = `The response must start with a JSON object enclosed in \`\`\`json ... \`\`\` containing fields: priority, complexity, tags, estimatedEffort, suggestedSubtasks. Example: \`\`\`json { "priority": "medium", "complexity": 5, "tags": ["ui", "refactor"], "estimatedEffort": "1 day", "suggestedSubtasks": [{"title": "Subtask 1", "description": "Desc 1"}] } \`\`\` The rest of the response should be Markdown.`; try { const refinedPromptText = await this.llmManager.refinePrompt(prompt, llmOutput, desiredSpec); this.addTaskNote(task.id, refinedPromptText, 'AI_PROMPT_REFINER', 'comment'); logger.info(`Successfully refined prompt for task ${task.id}`); } catch (refinementError) { logger.error(`Failed to refine prompt for task ${task.id}`, { error: refinementError }); break; } } } if (metadata) { if (metadata.priority && typeof metadata.priority === 'string') { task.priority = this.stringToPriority(metadata.priority) || task.priority; } if (metadata.complexity && typeof metadata.complexity === 'number') { task.complexity = Math.min(Math.max(1, Math.round(metadata.complexity)), 10); } if (metadata.tags && Array.isArray(metadata.tags)) { task.tags = metadata.tags.filter((tag) => typeof tag === 'string'); } if (metadata.estimatedEffort && typeof metadata.estimatedEffort === 'string') { task.estimatedEffort = metadata.estimatedEffort; } if (metadata.suggestedSubtasks && Array.isArray(metadata.suggestedSubtasks) && (!task.subtasks || task.subtasks.length === 0)) { task.subtasks = []; for (const subTaskData of metadata.suggestedSubtasks.slice(0, this.config.defaultSubtasks)) { if (subTaskData && typeof subTaskData.title === 'string' && typeof subTaskData.description === 'string') { const subId = await this.createSubtask(task.id, subTaskData.title, subTaskData.description); if (subId) { logger.info(`Created LLM suggested subtask ${subId} for task ${task.id}`); } } } } if (markdownContent) { this.addTaskNote(task.id, `# AI Generated Plan\n\n${markdownContent}`, 'AI_PLANNER', 'solution'); } task.updatedAt = Date.now(); logger.info(`Successfully enhanced task ${task.id} with LLM and parsed metadata.`); } else { logger.warn(`Failed to extract metadata from LLM response for task ${task.id} after all attempts: ${llmOutput.substring(0, 200)}...`); this.setDefaultTaskMetadata(task); this.addTaskNote(task.id, `# AI Enhancement Attempt (Metadata Failed)\n\n${llmOutput}`, 'SYSTEM_ERROR', 'comment'); } } catch (llmRequestError) { logger.error(`LLM request error during task enhancement for ${task.id}`, llmRequestError); this.setDefaultTaskMetadata(task); } } catch (outerError) { logger.error(`Unexpected error enhancing task ${task.id}`, { error: outerError }); this.setDefaultTaskMetadata(task); } }, ErrorCategory.LLM, 'enhance_task_with_llm', { taskId: task.id }); } setDefaultTaskMetadata(task) { task.priority = task.priority || TaskPriority.MEDIUM; task.complexity = task.complexity || 5; task.tags = task.tags || []; task.updatedAt = Date.now(); logger.info(`Applied default metadata to task ${task.id} due to LLM enhancement failure`); } getTask(id) { return this.tasks.get(id); } updateTask(id, updates) { const task = this.tasks.get(id); if (!task) return undefined; const updatedTask = { ...task, ...updates, updatedAt: Date.now() }; this.tasks.set(id, updatedTask); this.saveTasks(); return updatedTask; } addTaskNote(taskId, content, author, type) { const task = this.tasks.get(taskId); if (!task) return undefined; const sanitizedContent = content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim(); const note = { id: `note-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, content: sanitizedContent, timestamp: Date.now(), author, type }; task.notes.push(note); task.updatedAt = Date.now(); this.saveTasks(); return note; } deleteTask(id) { if (!this.tasks.has(id)) return false; this.tasks.delete(id); for (const task of this.tasks.values()) { if (task.dependencies.includes(id)) { task.dependencies = task.dependencies.filter(depId => depId !== id); if (task.status === TaskStatus.BLOCKED) { if (task.dependencies.length === 0) { task.status = TaskStatus.TODO; } } } if (task.subtasks && task.subtasks.includes(id)) { task.subtasks = task.subtasks.filter(subId => subId !== id); } } this.saveTasks(); return true; } getTasks(options = {}) { const { status, priority, tags, sortBy = "priority", sortDirection = "desc" } = options; let filteredTasks = Array.from(this.tasks.values()); if (status) { const statusArray = Array.isArray(status) ? status : [status]; filteredTasks = filteredTasks.filter(task => statusArray.includes(task.status)); } if (priority) { const priorityArray = Array.isArray(priority) ? priority : [priority]; filteredTasks = filteredTasks.filter(task => priorityArray.includes(task.priority)); } if (tags && tags.length > 0) { filteredTasks = filteredTasks.filter(task => tags.some(tag => task.tags.includes(tag))); } filteredTasks.sort((a, b) => { let valueA, valueB; switch (sortBy) { case "priority": const priorityOrder = { [TaskPriority.CRITICAL]: 0, [TaskPriority.HIGH]: 1, [TaskPriority.MEDIUM]: 2, [TaskPriority.LOW]: 3, [TaskPriority.BACKLOG]: 4 }; valueA = priorityOrder[a.priority]; valueB = priorityOrder[b.priority]; break; case "dueDate": valueA = a.dueDate || Number.MAX_SAFE_INTEGER; valueB = b.dueDate || Number.MAX_SAFE_INTEGER; break; case "createdAt": valueA = a.createdAt; valueB = b.createdAt; break; case "updatedAt": valueA = a.updatedAt; valueB = b.updatedAt; break; case "complexity": valueA = a.complexity; valueB = b.complexity; break; default: valueA = a.updatedAt; valueB = b.updatedAt; } return sortDirection === "asc" ? valueA - valueB : valueB - valueA; }); return filteredTasks; } getNextTask() { const todoTasks = this.getTasks({ status: TaskStatus.TODO, sortBy: "priority" }); return todoTasks[0]; } getTasksNeedingAttention() { const blockedTasks = this.getTasks({ status: TaskStatus.BLOCKED }); const criticalTasks = this.getTasks({ priority: TaskPriority.CRITICAL, status: [TaskStatus.TODO, TaskStatus.IN_PROGRESS] }); const dueSoonTasks = this.getTasks().filter(task => { if (!task.dueDate) return false; const dueIn48Hours = task.dueDate - Date.now() < 1000 * 60 * 60 * 48; return dueIn48Hours && task.status !== TaskStatus.DONE; }); const allTasks = [...blockedTasks, ...criticalTasks, ...dueSoonTasks]; const uniqueTasks = Array.from(new Map(allTasks.map(task => [task.id, task])).values()); return this.sortTasksByPriority(uniqueTasks); } sortTasksByPriority(tasks) { return [...tasks].sort((a, b) => { const priorityOrder = { [TaskPriority.CRITICAL]: 0, [TaskPriority.HIGH]: 1, [TaskPriority.MEDIUM]: 2, [TaskPriority.LOW]: 3, [TaskPriority.BACKLOG]: 4 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); } extractJsonFromText(text) { return JsonUtils.extractJsonArray(text, false); } async parsePRD(content) { return ErrorHandler.tryCatch(async () => { console.log(`Starting parsePRD in TaskManager with content length: ${content.length}`); if (!this.initialized && this.tasks.size === 0) { const workspaceBeforeRedetection = this.workspaceRoot; this.workspaceRoot = detectWorkspaceDirectory(); if (this.workspaceRoot !== workspaceBeforeRedetection) { logger.info(`Re-detected workspace root: ${this.workspaceRoot}`); } else { logger.info(`Keeping current workspace root: ${this.workspaceRoot}`); } try { process.chdir(this.workspaceRoot); logger.info(`Changed process.cwd() to workspace root: ${process.cwd()}`); } catch (err) { logger.warn(`Failed to change process.cwd() to workspace root: ${err}`); } if (!this.tasksFilePath || this.tasksFilePath === path.join(process.cwd(), this.config.tasksFileName)) { const absoluteTasksPath = path.join(this.workspaceRoot, this.config.tasksFileName); this.tasksFilePath = absoluteTasksPath; logger.info(`Updated tasks file path to absolute path: ${this.tasksFilePath}`); const dirPath = path.dirname(this.tasksFilePath); if (!fs.existsSync(dirPath)) { logger.info(`Creating directory for tasks file: ${dirPath}`); try { fs.mkdirSync(dirPath, { recursive: true }); } catch (err) { logger.warn(`Failed to create directory: ${err}`); } } } } const prompt = ` CRITICAL SYSTEM INSTRUCTION: You are an expert system that parses Product Requirements Documents (PRDs) and extracts actionable, high-quality, well-defined development tasks in a structured JSON format. Your ENTIRE response MUST be ONLY a valid JSON array of task objects, with ABSOLUTELY NOTHING else (no introductory text, no explanations, no markdown, no code block specifiers like \`\`\`json). # INPUT DOCUMENT (PRD): \`\`\` ${content} \`\`\` # TASK EXTRACTION GUIDELINES: 1. Identify distinct features, user stories, epics, or key requirements from the PRD. 2. For each, create a task object. Tasks should represent manageable, yet meaningful, units of work. Break down very large features into multiple, more granular tasks if appropriate for clarity and actionability. 3. **Titles** should be concise, highly descriptive, and action-oriented (e.g., "Implement user login via email/password", "Design database schema for product inventory"). Ensure titles clearly reflect the core purpose of the task. 4. **Descriptions** should comprehensively summarize the relevant section of the PRD, clearly state the task's objective, key deliverables, and any important context. If the PRD mentions specific components or modules, reference them. 5. **Priority, Complexity, Tags**: Infer these based on the PRD's emphasis, implied effort, and common software development best practices. Strive for realistic complexity assessments. - Priority: Indicate the relative importance and urgency. - Complexity: Reflect the estimated effort and intricacy. - Tags: Use relevant keywords that categorize the task (e.g., module name, feature type, technology). 6. **Acceptance Criteria**: Extract or formulate 3-5 specific, testable, and verifiable acceptance criteria for each task. These criteria should clearly define what "done" means for the task. If not clearly defined in the PRD, formulate them based on a reasonable interpretation of the requirements. Each criterion should be a distinct statement. # STRICTLY ENFORCE JSON OUTPUT FORMAT: 1. The response MUST start with an opening square bracket \'[\'. 2. The response MUST end with a closing square bracket \']\'. 3. There must be NO text or characters whatsoever before the initial \'[\' or after the final \']\'. 4. Each task object within the array must adhere to the specified schema. # REQUIRED JSON SCHEMA FOR EACH TASK OBJECT: { "title": "string (Concise, action-oriented, and highly descriptive task title)", "description": "string (Comprehensive description: objective, deliverables, PRD context. Min 20 words.)", "priority": "string (enum: \'critical\', \'high\', \'medium\', \'low\', \'backlog\')", "complexity": "number (integer 1-10, realistic assessment)", "tags": "array of strings (e.g., [\\\"frontend\\\", \\\"api\\\", \\\"database\\\", \\\"user-auth\\\"])", "acceptanceCriteria": "array of strings (3-5 specific, testable criteria, e.g., [\\\"User can log in with valid credentials\\\", \\\"Error shown for invalid credentials\\\", \\\"Successful login redirects to dashboard\\\"])" } # EXAMPLE OF A SINGLE TASK OBJECT (Illustrative): { "title": "Develop User Profile Page API Endpoints",