import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import fs from 'fs/promises'; import path from 'path'; import axios from 'axios'; import boxen from 'boxen'; import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js'; export function createHistoryCommand(): Command { return new Command('history') .description('View the audit trail and architectural history of a task') .argument('[taskId]', 'Task ID (e.g. T-10) or UUID') .action(async (taskId) => { const spinner = ora('Retrieving history...').start(); try { const { projectId, apiKey, apiUrl } = getContext(); // 1. Resolve Task ID (same logic as plan.ts) let realTaskId = taskId; if (!taskId) { const contextPath = path.join(process.cwd(), '.rigstate', 'CURRENT_CONTEXT.md'); const contextExists = await fs.access(contextPath).then(() => true).catch(() => false); if (contextExists) { const content = await fs.readFile(contextPath, 'utf-8'); const match = content.match(/\*\*ID:\*\*\s*(.+?)(?:\s|\n|$)/i); if (match) realTaskId = match[1].trim(); } } if (!realTaskId) { spinner.fail(chalk.red('No Task ID provided and no active context found.')); return; } // 2. Fetch Plan and Post-Mortem spinner.text = `Fetching audit trail for ${realTaskId}...`; // Resolve UUID const lookup = await axios.get(`${apiUrl}/api/v1/roadmap?project_id=${projectId}`, { headers: { Authorization: `Bearer ${apiKey}` } }); const task = lookup.data.data.roadmap.find((t: any) => t.id === realTaskId || `T-${t.step_number}` === realTaskId || t.step_number.toString() === realTaskId ); if (!task) throw new Error(`Task ${realTaskId} not found.`); // 3. Display TUI spinner.stop(); const header = chalk.bold.blue(`📜 Audit Trail: ${task.title} (T-${task.step_number})`); const statusStr = task.status === 'COMPLETED' ? chalk.green('COMPLETED') : chalk.yellow(task.status); let output = `Status: ${statusStr}\n`; output += `Created: ${new Date(task.created_at).toLocaleString()}\n`; if (task.completed_at) output += `Finished: ${new Date(task.completed_at).toLocaleString()}\n`; output += `\n${chalk.bold('--- Initial Intent ---')}\n`; const planPath = path.join(process.cwd(), '.rigstate', 'plans', `t-${task.step_number}.md`); const planExists = await fs.access(planPath).then(() => true).catch(() => false); if (planExists) { const planContent = await fs.readFile(planPath, 'utf-8'); // Simple extraction of the analysis part const analysisMatch = planContent.match(/## 1\. 🔍 Analysis([\s\S]+?)##/); output += analysisMatch ? analysisMatch[1].trim() : 'Plan archive found, but analysis section missing.'; } else { output += chalk.dim('No local plan archive found.'); } if (task.post_mortem) { output += `\n\n${chalk.bold.magenta('--- Post-Mortem (Scribe Analysis) ---')}\n`; output += task.post_mortem; } else if (task.status === 'COMPLETED') { output += `\n\n${chalk.yellow('⚠ This task was completed before Phase 2.2. No post-mortem available.')}`; } else { output += `\n\n${chalk.dim('Task is still in progress. Post-mortem will be generated on finish.')}`; } console.log(boxen(output, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue', title: header })); } catch (error: any) { spinner.fail(chalk.red(`Failed to retrieve history: ${error.message}`)); } }); } function getContext() { const apiKey = getApiKey(); const apiUrl = getApiUrl(); const projectId = getProjectId(); if (!projectId) throw new Error('Project ID missing.'); return { projectId, apiKey, apiUrl }; }