claude-autopm 1.31.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,760 @@
1
+ /**
2
+ * TaskService - Task Management Service
3
+ *
4
+ * Pure service layer for task operations without any I/O operations.
5
+ * Follows 3-layer architecture: Service (logic) -> No direct I/O
6
+ *
7
+ * Provides 15 pure business logic methods organized into 5 categories:
8
+ *
9
+ * 1. Status Management (4 methods):
10
+ * - normalizeTaskStatus: Normalize status values to standard format
11
+ * - isTaskOpen: Check if task is in open state
12
+ * - isTaskClosed: Check if task is in closed state
13
+ * - categorizeTaskStatus: Categorize status into buckets
14
+ *
15
+ * 2. Task Parsing & Validation (4 methods):
16
+ * - parseTaskNumber: Extract task number from ID
17
+ * - parseTaskMetadata: Parse task frontmatter
18
+ * - validateTaskMetadata: Validate required fields
19
+ * - formatTaskId: Format number to task ID
20
+ *
21
+ * 3. Dependencies (3 methods):
22
+ * - parseDependencies: Parse dependency string to array
23
+ * - hasBlockingDependencies: Check if dependencies block task
24
+ * - validateDependencyFormat: Validate dependency format
25
+ *
26
+ * 4. Analytics & Statistics (4 methods):
27
+ * - calculateTaskCompletion: Calculate completion percentage
28
+ * - getTaskStatistics: Get comprehensive task statistics
29
+ * - sortTasksByPriority: Sort tasks by priority level
30
+ * - filterTasksByStatus: Filter tasks by status
31
+ *
32
+ * 5. Task Generation (2 methods):
33
+ * - generateTaskMetadata: Generate task frontmatter
34
+ * - generateTaskContent: Build complete task markdown
35
+ *
36
+ * Documentation Queries:
37
+ * - mcp://context7/agile/task-management - Task management best practices
38
+ * - mcp://context7/agile/task-tracking - Task tracking patterns
39
+ * - mcp://context7/agile/task-dependencies - Dependency management
40
+ * - mcp://context7/project-management/task-breakdown - Task breakdown patterns
41
+ * - mcp://context7/markdown/frontmatter - YAML frontmatter patterns
42
+ */
43
+
44
+ const PRDService = require('./PRDService');
45
+
46
+ class TaskService {
47
+ /**
48
+ * Create a new TaskService instance
49
+ *
50
+ * @param {Object} options - Configuration options
51
+ * @param {PRDService} options.prdService - PRDService instance for parsing
52
+ * @param {ConfigManager} options.configManager - Optional ConfigManager instance
53
+ * @param {string} options.defaultTaskType - Default task type (default: 'development')
54
+ * @param {string} options.defaultEffort - Default effort (default: '1d')
55
+ * @throws {Error} If PRDService is not provided or invalid
56
+ */
57
+ constructor(options = {}) {
58
+ if (!options.prdService) {
59
+ throw new Error('PRDService instance is required');
60
+ }
61
+
62
+ if (!(options.prdService instanceof PRDService)) {
63
+ throw new Error('prdService must be an instance of PRDService');
64
+ }
65
+
66
+ this.prdService = options.prdService;
67
+
68
+ // Store ConfigManager if provided (for future use)
69
+ this.configManager = options.configManager || undefined;
70
+
71
+ this.options = {
72
+ defaultTaskType: 'development',
73
+ defaultEffort: '1d',
74
+ ...options
75
+ };
76
+
77
+ // Task ID counter for generation
78
+ this._taskIdCounter = Date.now() % 10000;
79
+ }
80
+
81
+ // ==========================================
82
+ // 1. STATUS MANAGEMENT (4 METHODS)
83
+ // ==========================================
84
+
85
+ /**
86
+ * Normalize task status to standard values
87
+ *
88
+ * Maps various status strings to standardized values:
89
+ * - "closed", "done", "finished" -> "completed"
90
+ * - Other statuses remain unchanged
91
+ * - Defaults to "open" for unknown/null values
92
+ *
93
+ * @param {string} status - Raw status string
94
+ * @returns {string} Normalized status
95
+ *
96
+ * @example
97
+ * normalizeTaskStatus('closed') // Returns 'completed'
98
+ * normalizeTaskStatus('done') // Returns 'completed'
99
+ * normalizeTaskStatus('open') // Returns 'open'
100
+ */
101
+ normalizeTaskStatus(status) {
102
+ const lowerStatus = (status || '').toLowerCase();
103
+
104
+ // Map closed variants to completed
105
+ if (['closed', 'done', 'finished'].includes(lowerStatus)) {
106
+ return 'completed';
107
+ }
108
+
109
+ // Keep known statuses
110
+ if (['completed', 'open', 'in_progress', 'blocked'].includes(lowerStatus)) {
111
+ return lowerStatus;
112
+ }
113
+
114
+ // Default to open for unknown
115
+ return 'open';
116
+ }
117
+
118
+ /**
119
+ * Check if task is in open/active state
120
+ *
121
+ * Returns true for: open, in_progress, blocked
122
+ * Returns false for: completed, closed, done
123
+ *
124
+ * @param {Object} task - Task object with status field
125
+ * @returns {boolean} True if task is open/active
126
+ *
127
+ * @example
128
+ * isTaskOpen({ status: 'open' }) // Returns true
129
+ * isTaskOpen({ status: 'in_progress' }) // Returns true
130
+ * isTaskOpen({ status: 'completed' }) // Returns false
131
+ */
132
+ isTaskOpen(task) {
133
+ const status = (task?.status || '').toLowerCase();
134
+ const normalizedStatus = this.normalizeTaskStatus(status);
135
+
136
+ // Open states: open, in_progress, blocked
137
+ return ['open', 'in_progress', 'blocked'].includes(normalizedStatus);
138
+ }
139
+
140
+ /**
141
+ * Check if task is in closed/completed state
142
+ *
143
+ * Returns true for: completed, closed, done
144
+ * Returns false for: open, in_progress, blocked
145
+ *
146
+ * @param {Object} task - Task object with status field
147
+ * @returns {boolean} True if task is closed/completed
148
+ *
149
+ * @example
150
+ * isTaskClosed({ status: 'completed' }) // Returns true
151
+ * isTaskClosed({ status: 'closed' }) // Returns true
152
+ * isTaskClosed({ status: 'open' }) // Returns false
153
+ */
154
+ isTaskClosed(task) {
155
+ const status = (task?.status || '').toLowerCase();
156
+ const normalizedStatus = this.normalizeTaskStatus(status);
157
+
158
+ // Closed state: completed
159
+ return normalizedStatus === 'completed';
160
+ }
161
+
162
+ /**
163
+ * Categorize task status into standard buckets
164
+ *
165
+ * Maps various status strings to standardized categories:
166
+ * - todo: open, not_started
167
+ * - in_progress: in_progress, active
168
+ * - completed: completed, done, closed
169
+ * - blocked: blocked
170
+ *
171
+ * @param {string} status - Raw status string
172
+ * @returns {string} Categorized status (todo|in_progress|completed|blocked)
173
+ *
174
+ * @example
175
+ * categorizeTaskStatus('open') // Returns 'todo'
176
+ * categorizeTaskStatus('in_progress') // Returns 'in_progress'
177
+ * categorizeTaskStatus('done') // Returns 'completed'
178
+ * categorizeTaskStatus('blocked') // Returns 'blocked'
179
+ */
180
+ categorizeTaskStatus(status) {
181
+ const lowerStatus = (status || '').toLowerCase();
182
+
183
+ // Todo statuses
184
+ if (['open', 'not_started', ''].includes(lowerStatus)) {
185
+ return 'todo';
186
+ }
187
+
188
+ // In progress statuses
189
+ if (['in_progress', 'active'].includes(lowerStatus)) {
190
+ return 'in_progress';
191
+ }
192
+
193
+ // Completed statuses
194
+ if (['completed', 'done', 'closed', 'finished'].includes(lowerStatus)) {
195
+ return 'completed';
196
+ }
197
+
198
+ // Blocked status
199
+ if (lowerStatus === 'blocked') {
200
+ return 'blocked';
201
+ }
202
+
203
+ // Default to todo for unknown
204
+ return 'todo';
205
+ }
206
+
207
+ // ==========================================
208
+ // 2. TASK PARSING & VALIDATION (4 METHODS)
209
+ // ==========================================
210
+
211
+ /**
212
+ * Extract task number from task ID
213
+ *
214
+ * Extracts numeric portion from task IDs:
215
+ * - "TASK-123" -> 123
216
+ * - "task-456" -> 456
217
+ * - "TASK123" -> 123
218
+ *
219
+ * @param {string} taskId - Task identifier
220
+ * @returns {number|null} Task number or null if invalid
221
+ *
222
+ * @example
223
+ * parseTaskNumber('TASK-123') // Returns 123
224
+ * parseTaskNumber('task-456') // Returns 456
225
+ * parseTaskNumber('invalid') // Returns null
226
+ */
227
+ parseTaskNumber(taskId) {
228
+ if (!taskId || typeof taskId !== 'string') {
229
+ return null;
230
+ }
231
+
232
+ // Match TASK-N or TASKN format (case insensitive)
233
+ const match = taskId.match(/task-?(\d+)/i);
234
+ if (match) {
235
+ return parseInt(match[1], 10);
236
+ }
237
+
238
+ return null;
239
+ }
240
+
241
+ /**
242
+ * Parse task metadata from markdown content
243
+ *
244
+ * Extracts YAML frontmatter from task markdown.
245
+ * Uses PRDService for frontmatter parsing.
246
+ *
247
+ * @param {string} content - Task markdown content
248
+ * @returns {Object|null} Parsed metadata or null
249
+ *
250
+ * @example
251
+ * parseTaskMetadata(`---
252
+ * id: TASK-123
253
+ * title: My Task
254
+ * ---`)
255
+ * // Returns: { id: 'TASK-123', title: 'My Task' }
256
+ */
257
+ parseTaskMetadata(content) {
258
+ return this.prdService.parseFrontmatter(content);
259
+ }
260
+
261
+ /**
262
+ * Validate task metadata
263
+ *
264
+ * Checks for required fields:
265
+ * - id: Task identifier (must be TASK-N format)
266
+ * - title: Task title
267
+ * - type: Task type
268
+ *
269
+ * @param {Object} metadata - Task metadata object
270
+ * @returns {Object} Validation result:
271
+ * - valid: Boolean indicating if metadata is valid
272
+ * - errors: Array of error messages
273
+ *
274
+ * @example
275
+ * validateTaskMetadata({ id: 'TASK-123', title: 'Task', type: 'backend' })
276
+ * // Returns: { valid: true, errors: [] }
277
+ */
278
+ validateTaskMetadata(metadata) {
279
+ const errors = [];
280
+
281
+ // Required fields
282
+ if (!metadata.id) {
283
+ errors.push('Missing required field: id');
284
+ } else {
285
+ // Validate ID format
286
+ if (!metadata.id.match(/^TASK-\d+$/i)) {
287
+ errors.push(`Invalid task ID format: ${metadata.id}`);
288
+ }
289
+ }
290
+
291
+ if (!metadata.title) {
292
+ errors.push('Missing required field: title');
293
+ }
294
+
295
+ if (!metadata.type) {
296
+ errors.push('Missing required field: type');
297
+ }
298
+
299
+ return {
300
+ valid: errors.length === 0,
301
+ errors
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Format number to task ID
307
+ *
308
+ * Converts numeric task number to standard TASK-N format.
309
+ *
310
+ * @param {number|string} number - Task number
311
+ * @returns {string} Formatted task ID (TASK-N)
312
+ * @throws {Error} If number is invalid
313
+ *
314
+ * @example
315
+ * formatTaskId(123) // Returns 'TASK-123'
316
+ * formatTaskId('456') // Returns 'TASK-456'
317
+ */
318
+ formatTaskId(number) {
319
+ const num = parseInt(number, 10);
320
+
321
+ if (isNaN(num) || num < 0) {
322
+ throw new Error(`Invalid task number: ${number}`);
323
+ }
324
+
325
+ return `TASK-${num}`;
326
+ }
327
+
328
+ // ==========================================
329
+ // 3. DEPENDENCIES (3 METHODS)
330
+ // ==========================================
331
+
332
+ /**
333
+ * Parse dependency string to array
334
+ *
335
+ * Handles multiple formats:
336
+ * - Comma-separated: "TASK-1, TASK-2"
337
+ * - Array format: "[TASK-1, TASK-2]"
338
+ * - Single: "TASK-1"
339
+ *
340
+ * @param {string} dependencyString - Dependency string
341
+ * @returns {Array<string>} Array of task IDs
342
+ *
343
+ * @example
344
+ * parseDependencies('TASK-1, TASK-2') // Returns ['TASK-1', 'TASK-2']
345
+ * parseDependencies('[TASK-1, TASK-2]') // Returns ['TASK-1', 'TASK-2']
346
+ * parseDependencies('') // Returns []
347
+ */
348
+ parseDependencies(dependencyString) {
349
+ if (!dependencyString || typeof dependencyString !== 'string') {
350
+ return [];
351
+ }
352
+
353
+ // Remove array brackets if present
354
+ let cleaned = dependencyString.trim();
355
+ cleaned = cleaned.replace(/^\[|\]$/g, '');
356
+
357
+ // Split by comma and trim
358
+ const deps = cleaned.split(',')
359
+ .map(dep => dep.trim())
360
+ .filter(dep => dep.length > 0);
361
+
362
+ return deps;
363
+ }
364
+
365
+ /**
366
+ * Check if task has blocking dependencies
367
+ *
368
+ * A task is blocked if any of its dependencies are:
369
+ * - Not completed/closed
370
+ * - Not found in allTasks array
371
+ *
372
+ * @param {Object} task - Task object with dependencies field
373
+ * @param {Array<Object>} allTasks - Array of all tasks
374
+ * @returns {boolean} True if task has blocking dependencies
375
+ *
376
+ * @example
377
+ * hasBlockingDependencies(
378
+ * { id: 'TASK-3', dependencies: 'TASK-1, TASK-2' },
379
+ * [
380
+ * { id: 'TASK-1', status: 'completed' },
381
+ * { id: 'TASK-2', status: 'open' }
382
+ * ]
383
+ * )
384
+ * // Returns true (TASK-2 is still open)
385
+ */
386
+ hasBlockingDependencies(task, allTasks) {
387
+ if (!task.dependencies) {
388
+ return false;
389
+ }
390
+
391
+ const deps = this.parseDependencies(task.dependencies);
392
+
393
+ if (deps.length === 0) {
394
+ return false;
395
+ }
396
+
397
+ // Check each dependency
398
+ for (const depId of deps) {
399
+ const depTask = allTasks.find(t => t.id === depId);
400
+
401
+ // Blocked if dependency not found
402
+ if (!depTask) {
403
+ return true;
404
+ }
405
+
406
+ // Blocked if dependency not closed
407
+ if (!this.isTaskClosed(depTask)) {
408
+ return true;
409
+ }
410
+ }
411
+
412
+ return false;
413
+ }
414
+
415
+ /**
416
+ * Validate dependency format
417
+ *
418
+ * Checks if dependency string follows valid formats:
419
+ * - Empty string (no dependencies)
420
+ * - TASK-N format
421
+ * - Comma-separated TASK-N
422
+ * - Array format [TASK-N, ...]
423
+ *
424
+ * @param {string} depString - Dependency string
425
+ * @returns {boolean} True if format is valid
426
+ *
427
+ * @example
428
+ * validateDependencyFormat('TASK-1') // Returns true
429
+ * validateDependencyFormat('TASK-1, TASK-2') // Returns true
430
+ * validateDependencyFormat('[TASK-1, TASK-2]') // Returns true
431
+ * validateDependencyFormat('invalid') // Returns false
432
+ */
433
+ validateDependencyFormat(depString) {
434
+ // Null/undefined is invalid
435
+ if (depString === null || depString === undefined) {
436
+ return false;
437
+ }
438
+
439
+ // Empty string is valid (no dependencies)
440
+ if (depString === '') {
441
+ return true;
442
+ }
443
+
444
+ if (typeof depString !== 'string') {
445
+ return false;
446
+ }
447
+
448
+ // Remove array brackets
449
+ let cleaned = depString.trim();
450
+ cleaned = cleaned.replace(/^\[|\]$/g, '').trim();
451
+
452
+ // Empty after cleaning is valid
453
+ if (cleaned === '') {
454
+ return true;
455
+ }
456
+
457
+ // Split and check each dependency
458
+ const deps = cleaned.split(',').map(d => d.trim());
459
+
460
+ for (const dep of deps) {
461
+ // Must match TASK-N format (case sensitive - uppercase TASK only)
462
+ if (!dep.match(/^TASK-\d+$/)) {
463
+ return false;
464
+ }
465
+ }
466
+
467
+ return true;
468
+ }
469
+
470
+ // ==========================================
471
+ // 4. ANALYTICS & STATISTICS (4 METHODS)
472
+ // ==========================================
473
+
474
+ /**
475
+ * Calculate task completion percentage
476
+ *
477
+ * Calculates percentage of closed tasks vs total tasks.
478
+ * Returns 0 for empty array.
479
+ *
480
+ * @param {Array<Object>} tasks - Array of task objects
481
+ * @returns {number} Completion percentage (0-100)
482
+ *
483
+ * @example
484
+ * calculateTaskCompletion([
485
+ * { status: 'completed' },
486
+ * { status: 'completed' },
487
+ * { status: 'open' },
488
+ * { status: 'open' }
489
+ * ])
490
+ * // Returns 50
491
+ */
492
+ calculateTaskCompletion(tasks) {
493
+ if (!Array.isArray(tasks) || tasks.length === 0) {
494
+ return 0;
495
+ }
496
+
497
+ const closedCount = tasks.filter(task => this.isTaskClosed(task)).length;
498
+ const percent = Math.round((closedCount * 100) / tasks.length);
499
+
500
+ return percent;
501
+ }
502
+
503
+ /**
504
+ * Get comprehensive task statistics
505
+ *
506
+ * Calculates multiple metrics:
507
+ * - total: Total number of tasks
508
+ * - open: Number of open/active tasks
509
+ * - closed: Number of closed/completed tasks
510
+ * - blocked: Number of blocked tasks
511
+ * - completionPercentage: Completion percentage
512
+ *
513
+ * @param {Array<Object>} tasks - Array of task objects
514
+ * @returns {Object} Statistics object
515
+ *
516
+ * @example
517
+ * getTaskStatistics([
518
+ * { status: 'open' },
519
+ * { status: 'completed' },
520
+ * { status: 'blocked' }
521
+ * ])
522
+ * // Returns:
523
+ * // {
524
+ * // total: 3,
525
+ * // open: 2,
526
+ * // closed: 1,
527
+ * // blocked: 1,
528
+ * // completionPercentage: 33
529
+ * // }
530
+ */
531
+ getTaskStatistics(tasks) {
532
+ if (!Array.isArray(tasks) || tasks.length === 0) {
533
+ return {
534
+ total: 0,
535
+ open: 0,
536
+ closed: 0,
537
+ blocked: 0,
538
+ completionPercentage: 0
539
+ };
540
+ }
541
+
542
+ const total = tasks.length;
543
+ const closed = tasks.filter(task => this.isTaskClosed(task)).length;
544
+ const open = tasks.filter(task => this.isTaskOpen(task)).length;
545
+ const blocked = tasks.filter(task => {
546
+ const status = (task?.status || '').toLowerCase();
547
+ return status === 'blocked';
548
+ }).length;
549
+
550
+ const completionPercentage = Math.round((closed * 100) / total);
551
+
552
+ return {
553
+ total,
554
+ open,
555
+ closed,
556
+ blocked,
557
+ completionPercentage
558
+ };
559
+ }
560
+
561
+ /**
562
+ * Sort tasks by priority
563
+ *
564
+ * Sorts tasks in priority order:
565
+ * - P1 (highest)
566
+ * - P2 (medium)
567
+ * - P3 (lowest)
568
+ *
569
+ * Tasks without priority default to P3.
570
+ * Does not mutate original array.
571
+ *
572
+ * @param {Array<Object>} tasks - Array of task objects
573
+ * @returns {Array<Object>} Sorted array (new array)
574
+ *
575
+ * @example
576
+ * sortTasksByPriority([
577
+ * { id: 'TASK-1', priority: 'P3' },
578
+ * { id: 'TASK-2', priority: 'P1' },
579
+ * { id: 'TASK-3', priority: 'P2' }
580
+ * ])
581
+ * // Returns tasks in order: TASK-2 (P1), TASK-3 (P2), TASK-1 (P3)
582
+ */
583
+ sortTasksByPriority(tasks) {
584
+ if (!Array.isArray(tasks)) {
585
+ return [];
586
+ }
587
+
588
+ // Priority order map
589
+ const priorityOrder = {
590
+ 'P1': 1,
591
+ 'P2': 2,
592
+ 'P3': 3
593
+ };
594
+
595
+ // Create copy and sort
596
+ return [...tasks].sort((a, b) => {
597
+ const priorityA = priorityOrder[a.priority] || 3; // Default to P3
598
+ const priorityB = priorityOrder[b.priority] || 3;
599
+
600
+ return priorityA - priorityB;
601
+ });
602
+ }
603
+
604
+ /**
605
+ * Filter tasks by status
606
+ *
607
+ * Returns tasks matching the specified status.
608
+ * Case insensitive.
609
+ * Does not mutate original array.
610
+ *
611
+ * @param {Array<Object>} tasks - Array of task objects
612
+ * @param {string} status - Status to filter by
613
+ * @returns {Array<Object>} Filtered array (new array)
614
+ *
615
+ * @example
616
+ * filterTasksByStatus([
617
+ * { id: 'TASK-1', status: 'open' },
618
+ * { id: 'TASK-2', status: 'completed' },
619
+ * { id: 'TASK-3', status: 'open' }
620
+ * ], 'open')
621
+ * // Returns [TASK-1, TASK-3]
622
+ */
623
+ filterTasksByStatus(tasks, status) {
624
+ if (!Array.isArray(tasks)) {
625
+ return [];
626
+ }
627
+
628
+ const targetStatus = (status || '').toLowerCase();
629
+
630
+ return tasks.filter(task => {
631
+ const taskStatus = (task?.status || '').toLowerCase();
632
+ return taskStatus === targetStatus;
633
+ });
634
+ }
635
+
636
+ // ==========================================
637
+ // 5. TASK GENERATION (2 METHODS)
638
+ // ==========================================
639
+
640
+ /**
641
+ * Generate task metadata
642
+ *
643
+ * Creates standardized task metadata with required and optional fields.
644
+ * Generates unique task ID if not provided.
645
+ *
646
+ * @param {string} title - Task title
647
+ * @param {Object} options - Optional metadata overrides
648
+ * @param {string} options.type - Task type (default: 'development')
649
+ * @param {string} options.effort - Effort estimate (default: '1d')
650
+ * @param {string} options.priority - Priority level (default: 'P2')
651
+ * @param {string} options.status - Task status (default: 'open')
652
+ * @param {string} options.dependencies - Dependencies
653
+ * @returns {Object} Task metadata object
654
+ * @throws {Error} If title is missing
655
+ *
656
+ * @example
657
+ * generateTaskMetadata('Implement feature', {
658
+ * type: 'backend',
659
+ * effort: '2d',
660
+ * priority: 'P1'
661
+ * })
662
+ * // Returns:
663
+ * // {
664
+ * // id: 'TASK-1234',
665
+ * // title: 'Implement feature',
666
+ * // type: 'backend',
667
+ * // effort: '2d',
668
+ * // status: 'open',
669
+ * // priority: 'P1',
670
+ * // created: '2025-01-01T00:00:00.000Z'
671
+ * // }
672
+ */
673
+ generateTaskMetadata(title, options = {}) {
674
+ if (!title || (typeof title === 'string' && title.trim() === '')) {
675
+ throw new Error('Task title is required');
676
+ }
677
+
678
+ // Generate unique task ID
679
+ this._taskIdCounter++;
680
+ const taskId = this.formatTaskId(this._taskIdCounter);
681
+
682
+ const metadata = {
683
+ id: taskId,
684
+ title,
685
+ type: options.type || this.options.defaultTaskType,
686
+ effort: options.effort || this.options.defaultEffort,
687
+ status: options.status || 'open',
688
+ priority: options.priority || 'P2',
689
+ created: new Date().toISOString()
690
+ };
691
+
692
+ // Add optional fields
693
+ if (options.dependencies) {
694
+ metadata.dependencies = options.dependencies;
695
+ }
696
+
697
+ return metadata;
698
+ }
699
+
700
+ /**
701
+ * Generate complete task markdown content
702
+ *
703
+ * Builds full task document with:
704
+ * - YAML frontmatter
705
+ * - Task title and description
706
+ * - Subtasks checklist
707
+ *
708
+ * @param {Object} metadata - Task metadata (frontmatter)
709
+ * @param {string} description - Task description (optional)
710
+ * @param {Array<string>} subtasks - Array of subtask strings (optional)
711
+ * @returns {string} Complete task markdown content
712
+ *
713
+ * @example
714
+ * generateTaskContent(
715
+ * { id: 'TASK-123', title: 'My Task', type: 'backend', ... },
716
+ * 'Task description',
717
+ * ['Subtask 1', 'Subtask 2']
718
+ * )
719
+ * // Returns multiline markdown with frontmatter, description, and subtasks
720
+ */
721
+ generateTaskContent(metadata, description = '', subtasks = []) {
722
+ // Build frontmatter
723
+ let frontmatter = `---
724
+ id: ${metadata.id}
725
+ title: ${metadata.title}
726
+ type: ${metadata.type}
727
+ effort: ${metadata.effort}
728
+ status: ${metadata.status}
729
+ priority: ${metadata.priority}
730
+ created: ${metadata.created}`;
731
+
732
+ // Add optional fields
733
+ if (metadata.dependencies) {
734
+ frontmatter += `\ndependencies: ${metadata.dependencies}`;
735
+ }
736
+
737
+ frontmatter += '\n---';
738
+
739
+ // Build content
740
+ let content = frontmatter + '\n\n';
741
+ content += `# ${metadata.id}: ${metadata.title}\n\n`;
742
+
743
+ // Add description
744
+ if (description && description.trim()) {
745
+ content += `## Description\n\n${description}\n\n`;
746
+ }
747
+
748
+ // Add subtasks
749
+ if (Array.isArray(subtasks) && subtasks.length > 0) {
750
+ content += '## Subtasks\n\n';
751
+ subtasks.forEach(subtask => {
752
+ content += `- [ ] ${subtask}\n`;
753
+ });
754
+ }
755
+
756
+ return content;
757
+ }
758
+ }
759
+
760
+ module.exports = TaskService;