import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import axios from 'axios'; import fs from 'fs/promises'; import path from 'path'; import inquirer from 'inquirer'; import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js'; export function createPlanCommand(): Command { const plan = new Command('plan'); plan .description('Generate an implementation plan for a roadmap task') .argument('[taskId]', 'Task ID (e.g. T-10) or UUID') .action(async (taskId) => { await executePlan(taskId); }); return plan; } export async function executePlan(taskId?: string) { const spinner = ora('Initializing Planning Mode...').start(); try { const { projectId, apiKey, apiUrl } = getContext(); // 1. Resolve Task ID if missing or short let realId = taskId; let taskTitle = ''; let taskDescription = ''; if (!taskId) { spinner.text = 'Fetching actionable tasks...'; // Interactive selection const response = await axios.get( `${apiUrl}/api/v1/roadmap?project_id=${projectId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); if (!response.data.success) throw new Error('Failed to fetch roadmap'); const tasks: any[] = response.data.data.roadmap || []; const choices = tasks .filter(t => ['ACTIVE', 'IN_PROGRESS', 'PENDING', 'LOCKED'].includes(t.status)) .map(t => ({ name: `${t.status === 'LOCKED' ? '🔒 ' : ''}T-${t.step_number}: ${t.title}${t.origin_id === 'GENESIS' ? chalk.dim(' [Foundation]') : ''}`, value: t })); if (choices.length === 0) { spinner.fail('No actionable tasks found in roadmap.'); return; } spinner.stop(); const answer = await inquirer.prompt([{ type: 'list', name: 'task', message: 'Select a task to plan:', choices }]); realId = answer.task.id; taskTitle = answer.task.title; taskDescription = answer.task.description; taskId = `T-${answer.task.step_number}`; } else { // Fetch specific task details spinner.text = `Fetching details for ${taskId}...`; const response = await axios.get( `${apiUrl}/api/v1/roadmap?project_id=${projectId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const task = response.data.data.roadmap.find((t: any) => t.id === taskId || `T-${t.step_number}` === taskId || t.step_number.toString() === taskId ); if (!task) throw new Error(`Task ${taskId} not found.`); realId = task.id; taskTitle = task.title; taskDescription = task.description; } // 2. Proactive Memory Injection (The Wisdom of Frank) spinner.start('Consulting the Project Brain for relevant advice...'); try { const memoryResponse = await axios.get( `${apiUrl}/api/v1/brain/search?project_id=${projectId}&query=${encodeURIComponent(taskTitle)}`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const memories = memoryResponse.data.memories || []; if (memories.length > 0) { spinner.info(chalk.yellow(`💡 wisdom of Frank: Found ${memories.length} relevant lessons from past tasks.`)); memories.forEach((m: any) => { console.log(chalk.dim(' • ') + chalk.italic(m.content)); }); console.log(''); } else { spinner.stop(); } } catch (e) { spinner.stop(); } // 3. Generate Context File spinner.start('Generating Context for Frank...'); const contextPath = path.join(process.cwd(), '.rigstate', 'CURRENT_CONTEXT.md'); const contextContent = ` # 🎯 Active Mission: ${taskTitle} **ID:** ${taskId} ## 📝 Description ${taskDescription} ## 🛡️ Architectural Constraints - Follow strictly the rules in .cursor/rules/ - Ensure zero violations in ACTIVE_VIOLATIONS.md - Update IMPLEMENTATION_PLAN.md before writing code. *Generated by Rigstate CLI at ${new Date().toLocaleString()}* `; await fs.mkdir(path.dirname(contextPath), { recursive: true }); await fs.writeFile(contextPath, contextContent.trim()); // 3. Handle Plan Mirroring (Hybrid Model) const planPath = path.join(process.cwd(), 'IMPLEMENTATION_PLAN.md'); const archiveDir = path.join(process.cwd(), '.rigstate', 'plans'); await fs.mkdir(archiveDir, { recursive: true }); // Resolve safe archive path const safeId = taskId!.replace(/[^a-z0-9]/gi, '-').toLowerCase(); const archivePath = path.join(archiveDir, `${safeId}.md`); // Fetch existing task to see if it has a plan/checklist const taskResponse = await axios.get( `${apiUrl}/api/v1/roadmap?project_id=${projectId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const fullTask = taskResponse.data.data.roadmap.find((t: any) => t.id === realId); let planContent = ''; // Prioritization for plan content: // 1. Local Archive (most up-to-date if Git-synced) // 2. Cloud Full Plan Text // 3. Cloud Checklist (generated fallback) // 4. Default Template const archiveExists = await fs.access(archivePath).then(() => true).catch(() => false); if (archiveExists) { spinner.info(chalk.dim('Restoring plan from local archive...')); planContent = await fs.readFile(archivePath, 'utf-8'); } else if (fullTask?.implementation_plan) { spinner.info(chalk.dim('Fetching full plan from cloud...')); planContent = fullTask.implementation_plan; } else if (fullTask?.checklist && fullTask.checklist.length > 0) { spinner.info(chalk.dim('Generating plan from cloud checklist...')); const checklistMd = fullTask.checklist.map((item: any) => `- [${item.checked ? 'x' : ' '}] ${item.text}`).join('\n'); planContent = ` # 📋 Implementation Plan: ${taskTitle} **Task ID:** ${taskId} ${checklistMd} `.trim(); } else { spinner.info(chalk.dim('Initializing new plan template...')); planContent = ` # 📋 Implementation Plan: ${taskTitle} **Task ID:** ${taskId} ## 1. 🔍 Analysis - [ ] Understand the requirements in .rigstate/CURRENT_CONTEXT.md - [ ] Check for existing architectural patterns ## 2. 🏗️ Proposed Changes [Frank: List the files you intend to modify and the nature of the changes] ## 3. ✅ Verification - [ ] Run tests ## 4. 🚀 Execution [Frank: Log your progress here]`.trim(); } // Write to Archive AND Mirror to root await fs.writeFile(archivePath, planContent); await fs.writeFile(planPath, planContent); spinner.succeed(chalk.green(`Workspace switched to ${taskId}`)); // 4. Update Status (Optional - maybe set to IN_PROGRESS?) // For now, let's keep it pure planning. // 4. Trigger Bridge Task (Autonomous Bridge) try { spinner.start('📡 Signaling Frank to start drafting...'); await axios.post(`${apiUrl}/api/v1/agent/bridge`, { project_id: projectId, task_id: realId, status: 'APPROVED', proposal: `draft_plan:${taskId}`, summary: `Requesting implementation plan for ${taskTitle}` }, { headers: { 'Authorization': `Bearer ${apiKey}` } }); spinner.succeed('Signal sent to Agent Bridge.'); } catch (e) { spinner.info(chalk.dim('Agent Bridge signal skipped (non-critical).')); } console.log(''); console.log(chalk.bold.blue('🚀 Planning Mode Activated')); console.log(chalk.dim('────────────────────────────────────────')); console.log(`1. Context loaded into: ${chalk.bold('.rigstate/CURRENT_CONTEXT.md')}`); console.log(`2. Plan template ready: ${chalk.bold('IMPLEMENTATION_PLAN.md')}`); console.log(''); console.log(chalk.green('✨ FRANK IS ON IT!')); console.log(chalk.dim(' He has received the bridge signal and will begin drafting shortly.')); console.log(chalk.dim(' No further manual steps required once he wakes up.')); console.log(''); } catch (e: any) { spinner.fail(chalk.red(`Planning failed: ${e.message}`)); } } function getContext() { const apiKey = getApiKey(); const apiUrl = getApiUrl(); const projectId = getProjectId(); if (!projectId) { throw new Error('Project ID missing. Run rigstate link.'); } return { projectId, apiKey, apiUrl }; }