#!/usr/bin/env npx ts-node /** * Task Management CLI * * Usage: npx ts-node task-cli.ts [feature] [args...] * * Commands: * status [feature] - Show task status summary * next [feature] - Show next eligible tasks * parallel [feature] - Show parallelizable tasks ready to run * deps - Show dependency tree for a task * blocked [feature] - Show blocked tasks and why * complete "summary" - Mark task completed * validate [feature] - Validate JSON files and dependencies * * Task files are stored in .tmp/tasks/ at the project root: * .tmp/tasks/{feature-slug}/task.json * .tmp/tasks/{feature-slug}/subtask_01.json * .tmp/tasks/completed/{feature-slug}/ */ const fs = require('fs'); const path = require('path'); // Find project root (look for .git or package.json) function findProjectRoot(): string { let dir = process.cwd(); while (dir !== path.dirname(dir)) { if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) { return dir; } dir = path.dirname(dir); } return process.cwd(); } const PROJECT_ROOT = findProjectRoot(); const TASKS_DIR = path.join(PROJECT_ROOT, '.tmp', 'tasks'); const COMPLETED_DIR = path.join(TASKS_DIR, 'completed'); interface Task { id: string; name: string; status: 'active' | 'completed' | 'blocked' | 'archived'; objective: string; context_files: string[]; reference_files?: string[]; exit_criteria: string[]; subtask_count: number; completed_count: number; created_at: string; completed_at: string | null; } interface Subtask { id: string; seq: string; title: string; status: 'pending' | 'in_progress' | 'completed' | 'blocked'; depends_on: string[]; parallel: boolean; context_files: string[]; reference_files?: string[]; acceptance_criteria: string[]; deliverables: string[]; agent_id: string | null; suggested_agent?: string; started_at: string | null; completed_at: string | null; completion_summary: string | null; } // Helpers function getFeatureDirs(): string[] { if (!fs.existsSync(TASKS_DIR)) return []; return fs.readdirSync(TASKS_DIR).filter((f: string) => { const fullPath = path.join(TASKS_DIR, f); return fs.statSync(fullPath).isDirectory() && f !== 'completed'; }); } function loadTask(feature: string): Task | null { const taskPath = path.join(TASKS_DIR, feature, 'task.json'); if (!fs.existsSync(taskPath)) return null; return JSON.parse(fs.readFileSync(taskPath, 'utf-8')); } function loadSubtasks(feature: string): Subtask[] { const featureDir = path.join(TASKS_DIR, feature); if (!fs.existsSync(featureDir)) return []; const files = fs.readdirSync(featureDir) .filter((f: string) => f.match(/^subtask_\d{2}\.json$/)) .sort(); return files.map((f: string) => JSON.parse(fs.readFileSync(path.join(featureDir, f), 'utf-8'))); } function saveSubtask(feature: string, subtask: Subtask): void { const subtaskPath = path.join(TASKS_DIR, feature, `subtask_${subtask.seq}.json`); fs.writeFileSync(subtaskPath, JSON.stringify(subtask, null, 2)); } function saveTask(feature: string, task: Task): void { const taskPath = path.join(TASKS_DIR, feature, 'task.json'); fs.writeFileSync(taskPath, JSON.stringify(task, null, 2)); } // Commands function cmdStatus(feature?: string): void { const features = feature ? [feature] : getFeatureDirs(); if (features.length === 0) { console.log('No active features found.'); return; } for (const f of features) { const task = loadTask(f); const subtasks = loadSubtasks(f); if (!task) { console.log(`\n[${f}] - No task.json found`); continue; } const counts = { pending: subtasks.filter(s => s.status === 'pending').length, in_progress: subtasks.filter(s => s.status === 'in_progress').length, completed: subtasks.filter(s => s.status === 'completed').length, blocked: subtasks.filter(s => s.status === 'blocked').length, }; const progress = subtasks.length > 0 ? Math.round((counts.completed / subtasks.length) * 100) : 0; console.log(`\n[${f}] ${task.name}`); console.log(` Status: ${task.status} | Progress: ${progress}% (${counts.completed}/${subtasks.length})`); console.log(` Pending: ${counts.pending} | In Progress: ${counts.in_progress} | Completed: ${counts.completed} | Blocked: ${counts.blocked}`); } } function cmdNext(feature?: string): void { const features = feature ? [feature] : getFeatureDirs(); console.log('\n=== Ready Tasks (deps satisfied) ===\n'); for (const f of features) { const subtasks = loadSubtasks(f); const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq)); const ready = subtasks.filter(s => { if (s.status !== 'pending') return false; return s.depends_on.every(dep => completedSeqs.has(dep)); }); if (ready.length > 0) { console.log(`[${f}]`); for (const s of ready) { const parallel = s.parallel ? '[parallel]' : '[sequential]'; console.log(` ${s.seq} - ${s.title} ${parallel}`); } console.log(); } } } function cmdParallel(feature?: string): void { const features = feature ? [feature] : getFeatureDirs(); console.log('\n=== Parallelizable Tasks Ready Now ===\n'); for (const f of features) { const subtasks = loadSubtasks(f); const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq)); const parallel = subtasks.filter(s => { if (s.status !== 'pending') return false; if (!s.parallel) return false; return s.depends_on.every(dep => completedSeqs.has(dep)); }); if (parallel.length > 0) { console.log(`[${f}] - ${parallel.length} parallel tasks:`); for (const s of parallel) { console.log(` ${s.seq} - ${s.title}`); } console.log(); } } } function cmdDeps(feature: string, seq: string): void { const subtasks = loadSubtasks(feature); const target = subtasks.find(s => s.seq === seq); if (!target) { console.log(`Task ${seq} not found in ${feature}`); return; } console.log(`\n=== Dependency Tree: ${feature}/${seq} ===\n`); console.log(`${seq} - ${target.title} [${target.status}]`); if (target.depends_on.length === 0) { console.log(' └── (no dependencies)'); return; } const printDeps = (seqs: string[], indent: string = ' '): void => { for (let i = 0; i < seqs.length; i++) { const depSeq = seqs[i]; const dep = subtasks.find(s => s.seq === depSeq); const isLast = i === seqs.length - 1; const branch = isLast ? '└──' : '├──'; if (dep) { const statusIcon = dep.status === 'completed' ? '✓' : dep.status === 'in_progress' ? '~' : '○'; console.log(`${indent}${branch} ${statusIcon} ${depSeq} - ${dep.title} [${dep.status}]`); if (dep.depends_on.length > 0) { const newIndent = indent + (isLast ? ' ' : '│ '); printDeps(dep.depends_on, newIndent); } } else { console.log(`${indent}${branch} ? ${depSeq} - NOT FOUND`); } } }; printDeps(target.depends_on); } function cmdBlocked(feature?: string): void { const features = feature ? [feature] : getFeatureDirs(); console.log('\n=== Blocked Tasks ===\n'); for (const f of features) { const subtasks = loadSubtasks(f); const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq)); const blocked = subtasks.filter(s => { if (s.status === 'blocked') return true; if (s.status !== 'pending') return false; return !s.depends_on.every(dep => completedSeqs.has(dep)); }); if (blocked.length > 0) { console.log(`[${f}]`); for (const s of blocked) { const waitingFor = s.depends_on.filter(dep => !completedSeqs.has(dep)); const reason = s.status === 'blocked' ? 'explicitly blocked' : `waiting: ${waitingFor.join(', ')}`; console.log(` ${s.seq} - ${s.title} (${reason})`); } console.log(); } } } function cmdComplete(feature: string, seq: string, summary: string): void { if (summary.length > 200) { console.log('Error: Summary must be max 200 characters'); process.exit(1); } const subtasks = loadSubtasks(feature); const subtask = subtasks.find(s => s.seq === seq); if (!subtask) { console.log(`Task ${seq} not found in ${feature}`); process.exit(1); } subtask.status = 'completed'; subtask.completed_at = new Date().toISOString(); subtask.completion_summary = summary; saveSubtask(feature, subtask); // Update task.json counts const task = loadTask(feature); if (task) { const newSubtasks = loadSubtasks(feature); task.completed_count = newSubtasks.filter(s => s.status === 'completed').length; saveTask(feature, task); } console.log(`\n✓ Marked ${feature}/${seq} as completed`); console.log(` Summary: ${summary}`); if (task) { console.log(` Progress: ${task.completed_count}/${task.subtask_count}`); } } function cmdValidate(feature?: string): void { const features = feature ? [feature] : getFeatureDirs(); let hasErrors = false; console.log('\n=== Validation Results ===\n'); for (const f of features) { const errors: string[] = []; const warnings: string[] = []; // Check task.json exists const task = loadTask(f); if (!task) { errors.push('Missing task.json'); } // Load and validate subtasks const subtasks = loadSubtasks(f); const seqs = new Set(subtasks.map(s => s.seq)); for (const s of subtasks) { // Check ID format if (!s.id.startsWith(f)) { errors.push(`${s.seq}: ID should start with feature name`); } // Check for missing dependencies for (const dep of s.depends_on) { if (!seqs.has(dep)) { errors.push(`${s.seq}: depends on non-existent task ${dep}`); } } // Check for circular dependencies const visited = new Set(); const checkCircular = (seq: string, path: string[]): boolean => { if (path.includes(seq)) { errors.push(`${s.seq}: circular dependency detected: ${[...path, seq].join(' -> ')}`); return true; } if (visited.has(seq)) return false; visited.add(seq); const task = subtasks.find(t => t.seq === seq); if (task) { for (const dep of task.depends_on) { if (checkCircular(dep, [...path, seq])) return true; } } return false; }; checkCircular(s.seq, []); // Warnings if (s.acceptance_criteria.length === 0) { warnings.push(`${s.seq}: No acceptance criteria defined`); } if (s.deliverables.length === 0) { warnings.push(`${s.seq}: No deliverables defined`); } } // Check counts match if (task && task.subtask_count !== subtasks.length) { errors.push(`task.json subtask_count (${task.subtask_count}) doesn't match actual count (${subtasks.length})`); } // Print results console.log(`[${f}]`); if (errors.length === 0 && warnings.length === 0) { console.log(' ✓ All checks passed'); } else { for (const e of errors) { console.log(` ✗ ERROR: ${e}`); hasErrors = true; } for (const w of warnings) { console.log(` ⚠ WARNING: ${w}`); } } console.log(); } process.exit(hasErrors ? 1 : 0); } // Main const [,, command, ...args] = process.argv; switch (command) { case 'status': cmdStatus(args[0]); break; case 'next': cmdNext(args[0]); break; case 'parallel': cmdParallel(args[0]); break; case 'deps': if (args.length < 2) { console.log('Usage: deps '); process.exit(1); } cmdDeps(args[0], args[1]); break; case 'blocked': cmdBlocked(args[0]); break; case 'complete': if (args.length < 3) { console.log('Usage: complete "summary"'); process.exit(1); } cmdComplete(args[0], args[1], args.slice(2).join(' ')); break; case 'validate': cmdValidate(args[0]); break; default: console.log(` Task Management CLI Usage: npx ts-node task-cli.ts [feature] [args...] Task files are stored in: .tmp/tasks/{feature-slug}/ Commands: status [feature] Show task status summary next [feature] Show next eligible tasks (deps satisfied) parallel [feature] Show parallelizable tasks ready to run deps Show dependency tree for a task blocked [feature] Show blocked tasks and why complete "summary" Mark task completed with summary validate [feature] Validate JSON files and dependencies Examples: npx ts-node task-cli.ts status npx ts-node task-cli.ts next my-feature npx ts-node task-cli.ts complete my-feature 02 "Implemented auth module" `); }