task-cli.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. #!/usr/bin/env npx ts-node
  2. /**
  3. * Task Management CLI
  4. *
  5. * Usage: npx ts-node task-cli.ts <command> [feature] [args...]
  6. *
  7. * Commands:
  8. * status [feature] - Show task status summary
  9. * next [feature] - Show next eligible tasks
  10. * parallel [feature] - Show parallelizable tasks ready to run
  11. * deps <feature> <seq> - Show dependency tree for a task
  12. * blocked [feature] - Show blocked tasks and why
  13. * complete <feature> <seq> "summary" - Mark task completed
  14. * validate [feature] - Validate JSON files and dependencies
  15. *
  16. * Task files are stored in .tmp/tasks/ at the project root:
  17. * .tmp/tasks/{feature-slug}/task.json
  18. * .tmp/tasks/{feature-slug}/subtask_01.json
  19. * .tmp/tasks/completed/{feature-slug}/
  20. */
  21. const fs = require('fs');
  22. const path = require('path');
  23. // Find project root (look for .git or package.json)
  24. function findProjectRoot(): string {
  25. let dir = process.cwd();
  26. while (dir !== path.dirname(dir)) {
  27. if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
  28. return dir;
  29. }
  30. dir = path.dirname(dir);
  31. }
  32. return process.cwd();
  33. }
  34. const PROJECT_ROOT = findProjectRoot();
  35. const TASKS_DIR = path.join(PROJECT_ROOT, '.tmp', 'tasks');
  36. const COMPLETED_DIR = path.join(TASKS_DIR, 'completed');
  37. interface Task {
  38. id: string;
  39. name: string;
  40. status: 'active' | 'completed' | 'blocked' | 'archived';
  41. objective: string;
  42. context_files: string[];
  43. exit_criteria: string[];
  44. subtask_count: number;
  45. completed_count: number;
  46. created_at: string;
  47. completed_at: string | null;
  48. }
  49. interface Subtask {
  50. id: string;
  51. seq: string;
  52. title: string;
  53. status: 'pending' | 'in_progress' | 'completed' | 'blocked';
  54. depends_on: string[];
  55. parallel: boolean;
  56. context_files: string[];
  57. acceptance_criteria: string[];
  58. deliverables: string[];
  59. agent_id: string | null;
  60. started_at: string | null;
  61. completed_at: string | null;
  62. completion_summary: string | null;
  63. }
  64. // Helpers
  65. function getFeatureDirs(): string[] {
  66. if (!fs.existsSync(TASKS_DIR)) return [];
  67. return fs.readdirSync(TASKS_DIR).filter((f: string) => {
  68. const fullPath = path.join(TASKS_DIR, f);
  69. return fs.statSync(fullPath).isDirectory() && f !== 'completed';
  70. });
  71. }
  72. function loadTask(feature: string): Task | null {
  73. const taskPath = path.join(TASKS_DIR, feature, 'task.json');
  74. if (!fs.existsSync(taskPath)) return null;
  75. return JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
  76. }
  77. function loadSubtasks(feature: string): Subtask[] {
  78. const featureDir = path.join(TASKS_DIR, feature);
  79. if (!fs.existsSync(featureDir)) return [];
  80. const files = fs.readdirSync(featureDir)
  81. .filter((f: string) => f.match(/^subtask_\d{2}\.json$/))
  82. .sort();
  83. return files.map((f: string) => JSON.parse(fs.readFileSync(path.join(featureDir, f), 'utf-8')));
  84. }
  85. function saveSubtask(feature: string, subtask: Subtask): void {
  86. const subtaskPath = path.join(TASKS_DIR, feature, `subtask_${subtask.seq}.json`);
  87. fs.writeFileSync(subtaskPath, JSON.stringify(subtask, null, 2));
  88. }
  89. function saveTask(feature: string, task: Task): void {
  90. const taskPath = path.join(TASKS_DIR, feature, 'task.json');
  91. fs.writeFileSync(taskPath, JSON.stringify(task, null, 2));
  92. }
  93. // Commands
  94. function cmdStatus(feature?: string): void {
  95. const features = feature ? [feature] : getFeatureDirs();
  96. if (features.length === 0) {
  97. console.log('No active features found.');
  98. return;
  99. }
  100. for (const f of features) {
  101. const task = loadTask(f);
  102. const subtasks = loadSubtasks(f);
  103. if (!task) {
  104. console.log(`\n[${f}] - No task.json found`);
  105. continue;
  106. }
  107. const counts = {
  108. pending: subtasks.filter(s => s.status === 'pending').length,
  109. in_progress: subtasks.filter(s => s.status === 'in_progress').length,
  110. completed: subtasks.filter(s => s.status === 'completed').length,
  111. blocked: subtasks.filter(s => s.status === 'blocked').length,
  112. };
  113. const progress = subtasks.length > 0
  114. ? Math.round((counts.completed / subtasks.length) * 100)
  115. : 0;
  116. console.log(`\n[${f}] ${task.name}`);
  117. console.log(` Status: ${task.status} | Progress: ${progress}% (${counts.completed}/${subtasks.length})`);
  118. console.log(` Pending: ${counts.pending} | In Progress: ${counts.in_progress} | Completed: ${counts.completed} | Blocked: ${counts.blocked}`);
  119. }
  120. }
  121. function cmdNext(feature?: string): void {
  122. const features = feature ? [feature] : getFeatureDirs();
  123. console.log('\n=== Ready Tasks (deps satisfied) ===\n');
  124. for (const f of features) {
  125. const subtasks = loadSubtasks(f);
  126. const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
  127. const ready = subtasks.filter(s => {
  128. if (s.status !== 'pending') return false;
  129. return s.depends_on.every(dep => completedSeqs.has(dep));
  130. });
  131. if (ready.length > 0) {
  132. console.log(`[${f}]`);
  133. for (const s of ready) {
  134. const parallel = s.parallel ? '[parallel]' : '[sequential]';
  135. console.log(` ${s.seq} - ${s.title} ${parallel}`);
  136. }
  137. console.log();
  138. }
  139. }
  140. }
  141. function cmdParallel(feature?: string): void {
  142. const features = feature ? [feature] : getFeatureDirs();
  143. console.log('\n=== Parallelizable Tasks Ready Now ===\n');
  144. for (const f of features) {
  145. const subtasks = loadSubtasks(f);
  146. const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
  147. const parallel = subtasks.filter(s => {
  148. if (s.status !== 'pending') return false;
  149. if (!s.parallel) return false;
  150. return s.depends_on.every(dep => completedSeqs.has(dep));
  151. });
  152. if (parallel.length > 0) {
  153. console.log(`[${f}] - ${parallel.length} parallel tasks:`);
  154. for (const s of parallel) {
  155. console.log(` ${s.seq} - ${s.title}`);
  156. }
  157. console.log();
  158. }
  159. }
  160. }
  161. function cmdDeps(feature: string, seq: string): void {
  162. const subtasks = loadSubtasks(feature);
  163. const target = subtasks.find(s => s.seq === seq);
  164. if (!target) {
  165. console.log(`Task ${seq} not found in ${feature}`);
  166. return;
  167. }
  168. console.log(`\n=== Dependency Tree: ${feature}/${seq} ===\n`);
  169. console.log(`${seq} - ${target.title} [${target.status}]`);
  170. if (target.depends_on.length === 0) {
  171. console.log(' └── (no dependencies)');
  172. return;
  173. }
  174. const printDeps = (seqs: string[], indent: string = ' '): void => {
  175. for (let i = 0; i < seqs.length; i++) {
  176. const depSeq = seqs[i];
  177. const dep = subtasks.find(s => s.seq === depSeq);
  178. const isLast = i === seqs.length - 1;
  179. const branch = isLast ? '└──' : '├──';
  180. if (dep) {
  181. const statusIcon = dep.status === 'completed' ? '✓' : dep.status === 'in_progress' ? '~' : '○';
  182. console.log(`${indent}${branch} ${statusIcon} ${depSeq} - ${dep.title} [${dep.status}]`);
  183. if (dep.depends_on.length > 0) {
  184. const newIndent = indent + (isLast ? ' ' : '│ ');
  185. printDeps(dep.depends_on, newIndent);
  186. }
  187. } else {
  188. console.log(`${indent}${branch} ? ${depSeq} - NOT FOUND`);
  189. }
  190. }
  191. };
  192. printDeps(target.depends_on);
  193. }
  194. function cmdBlocked(feature?: string): void {
  195. const features = feature ? [feature] : getFeatureDirs();
  196. console.log('\n=== Blocked Tasks ===\n');
  197. for (const f of features) {
  198. const subtasks = loadSubtasks(f);
  199. const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
  200. const blocked = subtasks.filter(s => {
  201. if (s.status === 'blocked') return true;
  202. if (s.status !== 'pending') return false;
  203. return !s.depends_on.every(dep => completedSeqs.has(dep));
  204. });
  205. if (blocked.length > 0) {
  206. console.log(`[${f}]`);
  207. for (const s of blocked) {
  208. const waitingFor = s.depends_on.filter(dep => !completedSeqs.has(dep));
  209. const reason = s.status === 'blocked'
  210. ? 'explicitly blocked'
  211. : `waiting: ${waitingFor.join(', ')}`;
  212. console.log(` ${s.seq} - ${s.title} (${reason})`);
  213. }
  214. console.log();
  215. }
  216. }
  217. }
  218. function cmdComplete(feature: string, seq: string, summary: string): void {
  219. if (summary.length > 200) {
  220. console.log('Error: Summary must be max 200 characters');
  221. process.exit(1);
  222. }
  223. const subtasks = loadSubtasks(feature);
  224. const subtask = subtasks.find(s => s.seq === seq);
  225. if (!subtask) {
  226. console.log(`Task ${seq} not found in ${feature}`);
  227. process.exit(1);
  228. }
  229. subtask.status = 'completed';
  230. subtask.completed_at = new Date().toISOString();
  231. subtask.completion_summary = summary;
  232. saveSubtask(feature, subtask);
  233. // Update task.json counts
  234. const task = loadTask(feature);
  235. if (task) {
  236. const newSubtasks = loadSubtasks(feature);
  237. task.completed_count = newSubtasks.filter(s => s.status === 'completed').length;
  238. saveTask(feature, task);
  239. }
  240. console.log(`\n✓ Marked ${feature}/${seq} as completed`);
  241. console.log(` Summary: ${summary}`);
  242. if (task) {
  243. console.log(` Progress: ${task.completed_count}/${task.subtask_count}`);
  244. }
  245. }
  246. function cmdValidate(feature?: string): void {
  247. const features = feature ? [feature] : getFeatureDirs();
  248. let hasErrors = false;
  249. console.log('\n=== Validation Results ===\n');
  250. for (const f of features) {
  251. const errors: string[] = [];
  252. const warnings: string[] = [];
  253. // Check task.json exists
  254. const task = loadTask(f);
  255. if (!task) {
  256. errors.push('Missing task.json');
  257. }
  258. // Load and validate subtasks
  259. const subtasks = loadSubtasks(f);
  260. const seqs = new Set(subtasks.map(s => s.seq));
  261. for (const s of subtasks) {
  262. // Check ID format
  263. if (!s.id.startsWith(f)) {
  264. errors.push(`${s.seq}: ID should start with feature name`);
  265. }
  266. // Check for missing dependencies
  267. for (const dep of s.depends_on) {
  268. if (!seqs.has(dep)) {
  269. errors.push(`${s.seq}: depends on non-existent task ${dep}`);
  270. }
  271. }
  272. // Check for circular dependencies
  273. const visited = new Set<string>();
  274. const checkCircular = (seq: string, path: string[]): boolean => {
  275. if (path.includes(seq)) {
  276. errors.push(`${s.seq}: circular dependency detected: ${[...path, seq].join(' -> ')}`);
  277. return true;
  278. }
  279. if (visited.has(seq)) return false;
  280. visited.add(seq);
  281. const task = subtasks.find(t => t.seq === seq);
  282. if (task) {
  283. for (const dep of task.depends_on) {
  284. if (checkCircular(dep, [...path, seq])) return true;
  285. }
  286. }
  287. return false;
  288. };
  289. checkCircular(s.seq, []);
  290. // Warnings
  291. if (s.acceptance_criteria.length === 0) {
  292. warnings.push(`${s.seq}: No acceptance criteria defined`);
  293. }
  294. if (s.deliverables.length === 0) {
  295. warnings.push(`${s.seq}: No deliverables defined`);
  296. }
  297. }
  298. // Check counts match
  299. if (task && task.subtask_count !== subtasks.length) {
  300. errors.push(`task.json subtask_count (${task.subtask_count}) doesn't match actual count (${subtasks.length})`);
  301. }
  302. // Print results
  303. console.log(`[${f}]`);
  304. if (errors.length === 0 && warnings.length === 0) {
  305. console.log(' ✓ All checks passed');
  306. } else {
  307. for (const e of errors) {
  308. console.log(` ✗ ERROR: ${e}`);
  309. hasErrors = true;
  310. }
  311. for (const w of warnings) {
  312. console.log(` ⚠ WARNING: ${w}`);
  313. }
  314. }
  315. console.log();
  316. }
  317. process.exit(hasErrors ? 1 : 0);
  318. }
  319. // Main
  320. const [,, command, ...args] = process.argv;
  321. switch (command) {
  322. case 'status':
  323. cmdStatus(args[0]);
  324. break;
  325. case 'next':
  326. cmdNext(args[0]);
  327. break;
  328. case 'parallel':
  329. cmdParallel(args[0]);
  330. break;
  331. case 'deps':
  332. if (args.length < 2) {
  333. console.log('Usage: deps <feature> <seq>');
  334. process.exit(1);
  335. }
  336. cmdDeps(args[0], args[1]);
  337. break;
  338. case 'blocked':
  339. cmdBlocked(args[0]);
  340. break;
  341. case 'complete':
  342. if (args.length < 3) {
  343. console.log('Usage: complete <feature> <seq> "summary"');
  344. process.exit(1);
  345. }
  346. cmdComplete(args[0], args[1], args.slice(2).join(' '));
  347. break;
  348. case 'validate':
  349. cmdValidate(args[0]);
  350. break;
  351. default:
  352. console.log(`
  353. Task Management CLI
  354. Usage: npx ts-node task-cli.ts <command> [feature] [args...]
  355. Task files are stored in: .tmp/tasks/{feature-slug}/
  356. Commands:
  357. status [feature] Show task status summary
  358. next [feature] Show next eligible tasks (deps satisfied)
  359. parallel [feature] Show parallelizable tasks ready to run
  360. deps <feature> <seq> Show dependency tree for a task
  361. blocked [feature] Show blocked tasks and why
  362. complete <feature> <seq> "summary" Mark task completed with summary
  363. validate [feature] Validate JSON files and dependencies
  364. Examples:
  365. npx ts-node task-cli.ts status
  366. npx ts-node task-cli.ts next my-feature
  367. npx ts-node task-cli.ts complete my-feature 02 "Implemented auth module"
  368. `);
  369. }