task-cli.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  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. * context [feature] - Show bounded context breakdown
  16. * contracts [feature] - Show contract dependencies
  17. *
  18. * Task files are stored in .tmp/tasks/ at the project root:
  19. * .tmp/tasks/{feature-slug}/task.json
  20. * .tmp/tasks/{feature-slug}/subtask_01.json
  21. * .tmp/tasks/completed/{feature-slug}/
  22. */
  23. const fs = require('fs');
  24. const path = require('path');
  25. // Line-number validator (inline for CommonJS compatibility)
  26. function validateLineNumberFormat(lines: string | undefined): { valid: boolean; errors: string[]; warnings?: string[] } {
  27. const errors: string[] = [];
  28. const warnings: string[] = [];
  29. if (!lines || lines.trim() === '') {
  30. return { valid: true, errors: [] };
  31. }
  32. const trimmed = lines.trim();
  33. const invalidCharsPattern = /[^0-9,\-\s]/;
  34. if (invalidCharsPattern.test(trimmed)) {
  35. errors.push(`Invalid characters in line range: "${trimmed}". Only digits, commas, and hyphens are allowed.`);
  36. return { valid: false, errors, warnings };
  37. }
  38. if (trimmed.startsWith(',') || trimmed.endsWith(',')) {
  39. errors.push(`Line range cannot start or end with a comma: "${trimmed}"`);
  40. }
  41. if (trimmed.startsWith('-') || trimmed.endsWith('-')) {
  42. errors.push(`Line range cannot start or end with a hyphen: "${trimmed}"`);
  43. }
  44. if (trimmed.includes(',,')) {
  45. errors.push(`Line range contains consecutive commas: "${trimmed}"`);
  46. }
  47. if (trimmed.includes('--')) {
  48. errors.push(`Line range contains consecutive hyphens: "${trimmed}"`);
  49. }
  50. if (errors.length > 0) {
  51. return { valid: false, errors, warnings };
  52. }
  53. const segments = trimmed.split(',');
  54. for (const segment of segments) {
  55. const segmentTrimmed = segment.trim();
  56. if (segmentTrimmed === '') {
  57. errors.push(`Empty segment in line range: "${trimmed}"`);
  58. continue;
  59. }
  60. if (segmentTrimmed.includes('-')) {
  61. const parts = segmentTrimmed.split('-');
  62. if (parts.length !== 2) {
  63. errors.push(`Invalid range format: "${segmentTrimmed}". Expected format: "start-end"`);
  64. continue;
  65. }
  66. const startStr = parts[0].trim();
  67. const endStr = parts[1].trim();
  68. if (startStr === '' || endStr === '') {
  69. errors.push(`Range has empty start or end: "${segmentTrimmed}"`);
  70. continue;
  71. }
  72. const start = parseInt(startStr, 10);
  73. const end = parseInt(endStr, 10);
  74. if (isNaN(start) || isNaN(end)) {
  75. errors.push(`Non-numeric values in range: "${segmentTrimmed}"`);
  76. continue;
  77. }
  78. if (start < 1 || end < 1) {
  79. errors.push(`Line numbers must be positive: "${segmentTrimmed}"`);
  80. continue;
  81. }
  82. if (start > end) {
  83. errors.push(`Invalid range (start > end): "${segmentTrimmed}". Start must be less than or equal to end.`);
  84. continue;
  85. }
  86. if (start === end) {
  87. warnings.push(`Range "${segmentTrimmed}" has same start and end. Consider using single line format: "${start}"`);
  88. }
  89. } else {
  90. const lineNum = parseInt(segmentTrimmed, 10);
  91. if (isNaN(lineNum)) {
  92. errors.push(`Non-numeric line number: "${segmentTrimmed}"`);
  93. continue;
  94. }
  95. if (lineNum < 1) {
  96. errors.push(`Line number must be positive: "${segmentTrimmed}"`);
  97. continue;
  98. }
  99. }
  100. }
  101. return {
  102. valid: errors.length === 0,
  103. errors,
  104. warnings: warnings.length > 0 ? warnings : undefined
  105. };
  106. }
  107. // Find project root (look for .git or package.json)
  108. function findProjectRoot(): string {
  109. let dir = process.cwd();
  110. while (dir !== path.dirname(dir)) {
  111. if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
  112. return dir;
  113. }
  114. dir = path.dirname(dir);
  115. }
  116. return process.cwd();
  117. }
  118. const PROJECT_ROOT = findProjectRoot();
  119. const TASKS_DIR = path.join(PROJECT_ROOT, '.tmp', 'tasks');
  120. const COMPLETED_DIR = path.join(TASKS_DIR, 'completed');
  121. // Enhanced schema types
  122. interface ContextFileReference {
  123. path: string;
  124. lines?: string;
  125. reason?: string;
  126. }
  127. interface Contract {
  128. type: 'api' | 'interface' | 'event' | 'schema';
  129. name: string;
  130. path?: string;
  131. status: 'draft' | 'defined' | 'implemented' | 'verified';
  132. description?: string;
  133. }
  134. interface DesignComponent {
  135. type: 'figma' | 'wireframe' | 'mockup' | 'prototype' | 'sketch';
  136. url?: string;
  137. path?: string;
  138. description?: string;
  139. }
  140. interface ADRReference {
  141. id: string;
  142. path?: string;
  143. title?: string;
  144. decision?: string;
  145. }
  146. interface RICEScore {
  147. reach: number;
  148. impact: number;
  149. confidence: number;
  150. effort: number;
  151. score?: number;
  152. }
  153. interface WSJFScore {
  154. business_value: number;
  155. time_criticality: number;
  156. risk_reduction: number;
  157. job_size: number;
  158. score?: number;
  159. }
  160. interface Task {
  161. id: string;
  162. name: string;
  163. status: 'active' | 'completed' | 'blocked' | 'archived';
  164. objective: string;
  165. context_files?: (string | ContextFileReference)[];
  166. reference_files?: (string | ContextFileReference)[];
  167. exit_criteria?: string[];
  168. subtask_count?: number;
  169. completed_count?: number;
  170. created_at: string;
  171. completed_at?: string | null;
  172. // Enhanced fields
  173. bounded_context?: string;
  174. module?: string;
  175. vertical_slice?: string;
  176. contracts?: Contract[];
  177. design_components?: DesignComponent[];
  178. related_adrs?: ADRReference[];
  179. rice_score?: RICEScore;
  180. wsjf_score?: WSJFScore;
  181. release_slice?: string;
  182. }
  183. interface Subtask {
  184. id: string;
  185. seq: string;
  186. title: string;
  187. status: 'pending' | 'in_progress' | 'completed' | 'blocked';
  188. depends_on?: string[];
  189. parallel?: boolean;
  190. context_files?: (string | ContextFileReference)[];
  191. reference_files?: (string | ContextFileReference)[];
  192. acceptance_criteria?: string[];
  193. deliverables?: string[];
  194. agent_id?: string | null;
  195. suggested_agent?: string;
  196. started_at?: string | null;
  197. completed_at?: string | null;
  198. completion_summary?: string | null;
  199. // Enhanced fields
  200. bounded_context?: string;
  201. module?: string;
  202. vertical_slice?: string;
  203. contracts?: Contract[];
  204. design_components?: DesignComponent[];
  205. related_adrs?: ADRReference[];
  206. }
  207. // Helpers
  208. function getFeatureDirs(): string[] {
  209. if (!fs.existsSync(TASKS_DIR)) return [];
  210. return fs.readdirSync(TASKS_DIR).filter((f: string) => {
  211. const fullPath = path.join(TASKS_DIR, f);
  212. return fs.statSync(fullPath).isDirectory() && f !== 'completed';
  213. });
  214. }
  215. function loadTask(feature: string): Task | null {
  216. const taskPath = path.join(TASKS_DIR, feature, 'task.json');
  217. if (!fs.existsSync(taskPath)) return null;
  218. return JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
  219. }
  220. function loadSubtasks(feature: string): Subtask[] {
  221. const featureDir = path.join(TASKS_DIR, feature);
  222. if (!fs.existsSync(featureDir)) return [];
  223. const files = fs.readdirSync(featureDir)
  224. .filter((f: string) => f.match(/^subtask_\d{2}\.json$/))
  225. .sort();
  226. return files.map((f: string) => JSON.parse(fs.readFileSync(path.join(featureDir, f), 'utf-8')));
  227. }
  228. function saveSubtask(feature: string, subtask: Subtask): void {
  229. const subtaskPath = path.join(TASKS_DIR, feature, `subtask_${subtask.seq}.json`);
  230. fs.writeFileSync(subtaskPath, JSON.stringify(subtask, null, 2));
  231. }
  232. function saveTask(feature: string, task: Task): void {
  233. const taskPath = path.join(TASKS_DIR, feature, 'task.json');
  234. fs.writeFileSync(taskPath, JSON.stringify(task, null, 2));
  235. }
  236. // Commands
  237. function cmdStatus(feature?: string): void {
  238. const features = feature ? [feature] : getFeatureDirs();
  239. if (features.length === 0) {
  240. console.log('No active features found.');
  241. return;
  242. }
  243. for (const f of features) {
  244. const task = loadTask(f);
  245. const subtasks = loadSubtasks(f);
  246. if (!task) {
  247. console.log(`\n[${f}] - No task.json found`);
  248. continue;
  249. }
  250. const counts = {
  251. pending: subtasks.filter(s => s.status === 'pending').length,
  252. in_progress: subtasks.filter(s => s.status === 'in_progress').length,
  253. completed: subtasks.filter(s => s.status === 'completed').length,
  254. blocked: subtasks.filter(s => s.status === 'blocked').length,
  255. };
  256. const progress = subtasks.length > 0
  257. ? Math.round((counts.completed / subtasks.length) * 100)
  258. : 0;
  259. console.log(`\n[${f}] ${task.name}`);
  260. console.log(` Status: ${task.status} | Progress: ${progress}% (${counts.completed}/${subtasks.length})`);
  261. console.log(` Pending: ${counts.pending} | In Progress: ${counts.in_progress} | Completed: ${counts.completed} | Blocked: ${counts.blocked}`);
  262. // Display enhanced metadata if present
  263. if (task.bounded_context) {
  264. console.log(` Bounded Context: ${task.bounded_context}`);
  265. }
  266. if (task.module) {
  267. console.log(` Module: ${task.module}`);
  268. }
  269. if (task.vertical_slice) {
  270. console.log(` Vertical Slice: ${task.vertical_slice}`);
  271. }
  272. if (task.release_slice) {
  273. console.log(` Release: ${task.release_slice}`);
  274. }
  275. if (task.rice_score) {
  276. const score = task.rice_score.score ||
  277. ((task.rice_score.reach * task.rice_score.impact * (task.rice_score.confidence / 100)) / task.rice_score.effort);
  278. console.log(` RICE Score: ${score.toFixed(2)} (R:${task.rice_score.reach} I:${task.rice_score.impact} C:${task.rice_score.confidence}% E:${task.rice_score.effort})`);
  279. }
  280. if (task.wsjf_score) {
  281. const score = task.wsjf_score.score ||
  282. ((task.wsjf_score.business_value + task.wsjf_score.time_criticality + task.wsjf_score.risk_reduction) / task.wsjf_score.job_size);
  283. console.log(` WSJF Score: ${score.toFixed(2)} (BV:${task.wsjf_score.business_value} TC:${task.wsjf_score.time_criticality} RR:${task.wsjf_score.risk_reduction} JS:${task.wsjf_score.job_size})`);
  284. }
  285. if (task.contracts && task.contracts.length > 0) {
  286. console.log(` Contracts: ${task.contracts.length} (${task.contracts.filter(c => c.status === 'implemented').length} implemented)`);
  287. }
  288. }
  289. }
  290. function cmdNext(feature?: string): void {
  291. const features = feature ? [feature] : getFeatureDirs();
  292. console.log('\n=== Ready Tasks (deps satisfied) ===\n');
  293. for (const f of features) {
  294. const subtasks = loadSubtasks(f);
  295. const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
  296. const ready = subtasks.filter(s => {
  297. if (s.status !== 'pending') return false;
  298. return (s.depends_on || []).every(dep => completedSeqs.has(dep));
  299. });
  300. if (ready.length > 0) {
  301. console.log(`[${f}]`);
  302. for (const s of ready) {
  303. const parallel = s.parallel ? '[parallel]' : '[sequential]';
  304. console.log(` ${s.seq} - ${s.title} ${parallel}`);
  305. }
  306. console.log();
  307. }
  308. }
  309. }
  310. function cmdParallel(feature?: string): void {
  311. const features = feature ? [feature] : getFeatureDirs();
  312. console.log('\n=== Parallelizable Tasks Ready Now ===\n');
  313. for (const f of features) {
  314. const subtasks = loadSubtasks(f);
  315. const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
  316. const parallel = subtasks.filter(s => {
  317. if (s.status !== 'pending') return false;
  318. if (!s.parallel) return false;
  319. return (s.depends_on || []).every(dep => completedSeqs.has(dep));
  320. });
  321. if (parallel.length > 0) {
  322. console.log(`[${f}] - ${parallel.length} parallel tasks:`);
  323. for (const s of parallel) {
  324. console.log(` ${s.seq} - ${s.title}`);
  325. }
  326. console.log();
  327. }
  328. }
  329. }
  330. function cmdDeps(feature: string, seq: string): void {
  331. const subtasks = loadSubtasks(feature);
  332. const target = subtasks.find(s => s.seq === seq);
  333. if (!target) {
  334. console.log(`Task ${seq} not found in ${feature}`);
  335. return;
  336. }
  337. console.log(`\n=== Dependency Tree: ${feature}/${seq} ===\n`);
  338. console.log(`${seq} - ${target.title} [${target.status}]`);
  339. const depends_on = target.depends_on || [];
  340. if (depends_on.length === 0) {
  341. console.log(' └── (no dependencies)');
  342. return;
  343. }
  344. const printDeps = (seqs: string[], indent: string = ' '): void => {
  345. for (let i = 0; i < seqs.length; i++) {
  346. const depSeq = seqs[i];
  347. const dep = subtasks.find(s => s.seq === depSeq);
  348. const isLast = i === seqs.length - 1;
  349. const branch = isLast ? '└──' : '├──';
  350. if (dep) {
  351. const statusIcon = dep.status === 'completed' ? '✓' : dep.status === 'in_progress' ? '~' : '○';
  352. console.log(`${indent}${branch} ${statusIcon} ${depSeq} - ${dep.title} [${dep.status}]`);
  353. const depDeps = dep.depends_on || [];
  354. if (depDeps.length > 0) {
  355. const newIndent = indent + (isLast ? ' ' : '│ ');
  356. printDeps(depDeps, newIndent);
  357. }
  358. } else {
  359. console.log(`${indent}${branch} ? ${depSeq} - NOT FOUND`);
  360. }
  361. }
  362. };
  363. printDeps(depends_on);
  364. }
  365. function cmdBlocked(feature?: string): void {
  366. const features = feature ? [feature] : getFeatureDirs();
  367. console.log('\n=== Blocked Tasks ===\n');
  368. for (const f of features) {
  369. const subtasks = loadSubtasks(f);
  370. const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
  371. const blocked = subtasks.filter(s => {
  372. if (s.status === 'blocked') return true;
  373. if (s.status !== 'pending') return false;
  374. return !(s.depends_on || []).every(dep => completedSeqs.has(dep));
  375. });
  376. if (blocked.length > 0) {
  377. console.log(`[${f}]`);
  378. for (const s of blocked) {
  379. const waitingFor = (s.depends_on || []).filter(dep => !completedSeqs.has(dep));
  380. const reason = s.status === 'blocked'
  381. ? 'explicitly blocked'
  382. : `waiting: ${waitingFor.join(', ')}`;
  383. console.log(` ${s.seq} - ${s.title} (${reason})`);
  384. }
  385. console.log();
  386. }
  387. }
  388. }
  389. function cmdComplete(feature: string, seq: string, summary: string): void {
  390. if (summary.length > 200) {
  391. console.log('Error: Summary must be max 200 characters');
  392. process.exit(1);
  393. }
  394. const subtasks = loadSubtasks(feature);
  395. const subtask = subtasks.find(s => s.seq === seq);
  396. if (!subtask) {
  397. console.log(`Task ${seq} not found in ${feature}`);
  398. process.exit(1);
  399. return; // TypeScript guard
  400. }
  401. subtask.status = 'completed';
  402. subtask.completed_at = new Date().toISOString();
  403. subtask.completion_summary = summary;
  404. saveSubtask(feature, subtask);
  405. // Update task.json counts
  406. const task = loadTask(feature);
  407. if (task) {
  408. const newSubtasks = loadSubtasks(feature);
  409. task.completed_count = newSubtasks.filter(s => s.status === 'completed').length;
  410. saveTask(feature, task);
  411. }
  412. console.log(`\n✓ Marked ${feature}/${seq} as completed`);
  413. console.log(` Summary: ${summary}`);
  414. if (task) {
  415. console.log(` Progress: ${task.completed_count}/${task.subtask_count}`);
  416. }
  417. }
  418. function cmdValidate(feature?: string): void {
  419. const features = feature ? [feature] : getFeatureDirs();
  420. let hasErrors = false;
  421. console.log('\n=== Validation Results ===\n');
  422. for (const f of features) {
  423. const errors: string[] = [];
  424. const warnings: string[] = [];
  425. // Check task.json exists
  426. const task = loadTask(f);
  427. if (!task) {
  428. errors.push('Missing task.json');
  429. }
  430. // Load and validate subtasks
  431. const subtasks = loadSubtasks(f);
  432. const seqs = new Set(subtasks.map(s => s.seq));
  433. for (const s of subtasks) {
  434. // Check ID format
  435. if (!s.id.startsWith(f)) {
  436. errors.push(`${s.seq}: ID should start with feature name`);
  437. }
  438. // Check for missing dependencies
  439. for (const dep of (s.depends_on || [])) {
  440. if (!seqs.has(dep)) {
  441. errors.push(`${s.seq}: depends on non-existent task ${dep}`);
  442. }
  443. }
  444. // Check for circular dependencies
  445. const visited = new Set<string>();
  446. const checkCircular = (seq: string, path: string[]): boolean => {
  447. if (path.includes(seq)) {
  448. errors.push(`${s.seq}: circular dependency detected: ${[...path, seq].join(' -> ')}`);
  449. return true;
  450. }
  451. if (visited.has(seq)) return false;
  452. visited.add(seq);
  453. const task = subtasks.find(t => t.seq === seq);
  454. if (task) {
  455. for (const dep of (task.depends_on || [])) {
  456. if (checkCircular(dep, [...path, seq])) return true;
  457. }
  458. }
  459. return false;
  460. };
  461. checkCircular(s.seq, []);
  462. // Validate enhanced fields
  463. if (s.contracts) {
  464. for (const contract of s.contracts) {
  465. if (!['api', 'interface', 'event', 'schema'].includes(contract.type)) {
  466. errors.push(`${s.seq}: invalid contract type "${contract.type}"`);
  467. }
  468. if (!['draft', 'defined', 'implemented', 'verified'].includes(contract.status)) {
  469. errors.push(`${s.seq}: invalid contract status "${contract.status}"`);
  470. }
  471. }
  472. }
  473. if (s.design_components) {
  474. for (const comp of s.design_components) {
  475. if (!['figma', 'wireframe', 'mockup', 'prototype', 'sketch'].includes(comp.type)) {
  476. errors.push(`${s.seq}: invalid design component type "${comp.type}"`);
  477. }
  478. }
  479. }
  480. // Validate line-number format in context_files
  481. if (s.context_files) {
  482. for (const ref of s.context_files) {
  483. if (typeof ref !== 'string' && ref.lines) {
  484. const lineValidation = validateLineNumberFormat(ref.lines);
  485. if (!lineValidation.valid) {
  486. for (const error of lineValidation.errors) {
  487. errors.push(`${s.seq}: context_files line format error: ${error}`);
  488. }
  489. }
  490. if (lineValidation.warnings) {
  491. for (const warning of lineValidation.warnings) {
  492. warnings.push(`${s.seq}: context_files line format warning: ${warning}`);
  493. }
  494. }
  495. }
  496. }
  497. }
  498. // Validate line-number format in reference_files
  499. if (s.reference_files) {
  500. for (const ref of s.reference_files) {
  501. if (typeof ref !== 'string' && ref.lines) {
  502. const lineValidation = validateLineNumberFormat(ref.lines);
  503. if (!lineValidation.valid) {
  504. for (const error of lineValidation.errors) {
  505. errors.push(`${s.seq}: reference_files line format error: ${error}`);
  506. }
  507. }
  508. if (lineValidation.warnings) {
  509. for (const warning of lineValidation.warnings) {
  510. warnings.push(`${s.seq}: reference_files line format warning: ${warning}`);
  511. }
  512. }
  513. }
  514. }
  515. }
  516. // Warnings
  517. if (!(s.acceptance_criteria || []).length) {
  518. warnings.push(`${s.seq}: No acceptance criteria defined`);
  519. }
  520. if (!(s.deliverables || []).length) {
  521. warnings.push(`${s.seq}: No deliverables defined`);
  522. }
  523. }
  524. // Validate task-level enhanced fields
  525. if (task) {
  526. // Validate line-number format in task context_files
  527. if (task.context_files) {
  528. for (const ref of task.context_files) {
  529. if (typeof ref !== 'string' && ref.lines) {
  530. const lineValidation = validateLineNumberFormat(ref.lines);
  531. if (!lineValidation.valid) {
  532. for (const error of lineValidation.errors) {
  533. errors.push(`task.json: context_files line format error: ${error}`);
  534. }
  535. }
  536. if (lineValidation.warnings) {
  537. for (const warning of lineValidation.warnings) {
  538. warnings.push(`task.json: context_files line format warning: ${warning}`);
  539. }
  540. }
  541. }
  542. }
  543. }
  544. // Validate line-number format in task reference_files
  545. if (task.reference_files) {
  546. for (const ref of task.reference_files) {
  547. if (typeof ref !== 'string' && ref.lines) {
  548. const lineValidation = validateLineNumberFormat(ref.lines);
  549. if (!lineValidation.valid) {
  550. for (const error of lineValidation.errors) {
  551. errors.push(`task.json: reference_files line format error: ${error}`);
  552. }
  553. }
  554. if (lineValidation.warnings) {
  555. for (const warning of lineValidation.warnings) {
  556. warnings.push(`task.json: reference_files line format warning: ${warning}`);
  557. }
  558. }
  559. }
  560. }
  561. }
  562. if (task.rice_score) {
  563. const r = task.rice_score;
  564. if (r.reach <= 0) errors.push('RICE reach must be > 0');
  565. if (r.impact < 0.25 || r.impact > 3) errors.push('RICE impact must be 0.25-3');
  566. if (r.confidence < 0 || r.confidence > 100) errors.push('RICE confidence must be 0-100');
  567. if (r.effort <= 0) errors.push('RICE effort must be > 0');
  568. }
  569. if (task.wsjf_score) {
  570. const w = task.wsjf_score;
  571. if (w.business_value < 1 || w.business_value > 10) errors.push('WSJF business_value must be 1-10');
  572. if (w.time_criticality < 1 || w.time_criticality > 10) errors.push('WSJF time_criticality must be 1-10');
  573. if (w.risk_reduction < 1 || w.risk_reduction > 10) errors.push('WSJF risk_reduction must be 1-10');
  574. if (w.job_size < 1 || w.job_size > 10) errors.push('WSJF job_size must be 1-10');
  575. }
  576. if (task.contracts) {
  577. for (const contract of task.contracts) {
  578. if (!['api', 'interface', 'event', 'schema'].includes(contract.type)) {
  579. errors.push(`task.json: invalid contract type "${contract.type}"`);
  580. }
  581. if (!['draft', 'defined', 'implemented', 'verified'].includes(contract.status)) {
  582. errors.push(`task.json: invalid contract status "${contract.status}"`);
  583. }
  584. }
  585. }
  586. // Check counts match
  587. if (task.subtask_count !== subtasks.length) {
  588. errors.push(`task.json subtask_count (${task.subtask_count}) doesn't match actual count (${subtasks.length})`);
  589. }
  590. }
  591. // Print results
  592. console.log(`[${f}]`);
  593. if (errors.length === 0 && warnings.length === 0) {
  594. console.log(' ✓ All checks passed');
  595. } else {
  596. for (const e of errors) {
  597. console.log(` ✗ ERROR: ${e}`);
  598. hasErrors = true;
  599. }
  600. for (const w of warnings) {
  601. console.log(` ⚠ WARNING: ${w}`);
  602. }
  603. }
  604. console.log();
  605. }
  606. process.exit(hasErrors ? 1 : 0);
  607. }
  608. function cmdContext(feature?: string): void {
  609. const features = feature ? [feature] : getFeatureDirs();
  610. console.log('\n=== Bounded Context Breakdown ===\n');
  611. for (const f of features) {
  612. const task = loadTask(f);
  613. const subtasks = loadSubtasks(f);
  614. if (!task) continue;
  615. console.log(`[${f}] ${task.name}`);
  616. if (task.bounded_context) {
  617. console.log(` Bounded Context: ${task.bounded_context}`);
  618. }
  619. if (task.module) {
  620. console.log(` Module: ${task.module}`);
  621. }
  622. if (task.vertical_slice) {
  623. console.log(` Vertical Slice: ${task.vertical_slice}`);
  624. }
  625. // Group subtasks by bounded context
  626. const contextGroups = new Map<string, Subtask[]>();
  627. for (const s of subtasks) {
  628. const ctx = s.bounded_context || task.bounded_context || 'unspecified';
  629. if (!contextGroups.has(ctx)) {
  630. contextGroups.set(ctx, []);
  631. }
  632. contextGroups.get(ctx)!.push(s);
  633. }
  634. if (contextGroups.size > 1 || (contextGroups.size === 1 && !contextGroups.has('unspecified'))) {
  635. console.log('\n Subtasks by Context:');
  636. for (const [ctx, tasks] of contextGroups) {
  637. console.log(` ${ctx}: ${tasks.length} tasks`);
  638. for (const t of tasks) {
  639. const status = t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '~' : '○';
  640. console.log(` ${status} ${t.seq} - ${t.title}`);
  641. }
  642. }
  643. }
  644. console.log();
  645. }
  646. }
  647. function cmdContracts(feature?: string): void {
  648. const features = feature ? [feature] : getFeatureDirs();
  649. console.log('\n=== Contract Dependencies ===\n');
  650. for (const f of features) {
  651. const task = loadTask(f);
  652. const subtasks = loadSubtasks(f);
  653. if (!task) continue;
  654. const allContracts: Array<{source: string, contract: Contract}> = [];
  655. // Collect task-level contracts
  656. if (task.contracts) {
  657. for (const c of task.contracts) {
  658. allContracts.push({ source: 'task', contract: c });
  659. }
  660. }
  661. // Collect subtask-level contracts
  662. for (const s of subtasks) {
  663. if (s.contracts) {
  664. for (const c of s.contracts) {
  665. allContracts.push({ source: `subtask ${s.seq}`, contract: c });
  666. }
  667. }
  668. }
  669. if (allContracts.length === 0) {
  670. console.log(`[${f}] - No contracts defined`);
  671. continue;
  672. }
  673. console.log(`[${f}] ${task.name}`);
  674. console.log(` Total Contracts: ${allContracts.length}\n`);
  675. // Group by type
  676. const byType = new Map<string, Array<{source: string, contract: Contract}>>();
  677. for (const item of allContracts) {
  678. if (!byType.has(item.contract.type)) {
  679. byType.set(item.contract.type, []);
  680. }
  681. byType.get(item.contract.type)!.push(item);
  682. }
  683. for (const [type, items] of byType) {
  684. console.log(` ${type.toUpperCase()} Contracts (${items.length}):`);
  685. for (const { source, contract } of items) {
  686. const statusIcon = contract.status === 'verified' ? '✓' :
  687. contract.status === 'implemented' ? '~' :
  688. contract.status === 'defined' ? '○' : '◌';
  689. console.log(` ${statusIcon} ${contract.name} [${contract.status}] (${source})`);
  690. if (contract.description) {
  691. console.log(` ${contract.description}`);
  692. }
  693. if (contract.path) {
  694. console.log(` Path: ${contract.path}`);
  695. }
  696. }
  697. console.log();
  698. }
  699. }
  700. }
  701. // Main
  702. const [,, command, ...args] = process.argv;
  703. switch (command) {
  704. case 'status':
  705. cmdStatus(args[0]);
  706. break;
  707. case 'next':
  708. cmdNext(args[0]);
  709. break;
  710. case 'parallel':
  711. cmdParallel(args[0]);
  712. break;
  713. case 'deps':
  714. if (args.length < 2) {
  715. console.log('Usage: deps <feature> <seq>');
  716. process.exit(1);
  717. }
  718. cmdDeps(args[0], args[1]);
  719. break;
  720. case 'blocked':
  721. cmdBlocked(args[0]);
  722. break;
  723. case 'complete':
  724. if (args.length < 3) {
  725. console.log('Usage: complete <feature> <seq> "summary"');
  726. process.exit(1);
  727. }
  728. cmdComplete(args[0], args[1], args.slice(2).join(' '));
  729. break;
  730. case 'validate':
  731. cmdValidate(args[0]);
  732. break;
  733. case 'context':
  734. cmdContext(args[0]);
  735. break;
  736. case 'contracts':
  737. cmdContracts(args[0]);
  738. break;
  739. default:
  740. console.log(`
  741. Task Management CLI
  742. Usage: npx ts-node task-cli.ts <command> [feature] [args...]
  743. Task files are stored in: .tmp/tasks/{feature-slug}/
  744. Commands:
  745. status [feature] Show task status summary
  746. next [feature] Show next eligible tasks (deps satisfied)
  747. parallel [feature] Show parallelizable tasks ready to run
  748. deps <feature> <seq> Show dependency tree for a task
  749. blocked [feature] Show blocked tasks and why
  750. complete <feature> <seq> "summary" Mark task completed with summary
  751. validate [feature] Validate JSON files and dependencies
  752. context [feature] Show bounded context breakdown
  753. contracts [feature] Show contract dependencies
  754. Examples:
  755. npx ts-node task-cli.ts status
  756. npx ts-node task-cli.ts next my-feature
  757. npx ts-node task-cli.ts complete my-feature 02 "Implemented auth module"
  758. npx ts-node task-cli.ts context my-feature
  759. npx ts-node task-cli.ts contracts my-feature
  760. `);
  761. }