claude-autopm 1.31.0 → 2.1.1
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.
- package/README.md +57 -5
- package/autopm/.claude/mcp/test-server.md +10 -0
- package/bin/autopm-poc.js +176 -44
- package/bin/autopm.js +97 -179
- package/lib/ai-providers/AbstractAIProvider.js +524 -0
- package/lib/ai-providers/ClaudeProvider.js +359 -48
- package/lib/ai-providers/TemplateProvider.js +432 -0
- package/lib/cli/commands/agent.js +206 -0
- package/lib/cli/commands/config.js +488 -0
- package/lib/cli/commands/prd.js +345 -0
- package/lib/cli/commands/task.js +206 -0
- package/lib/config/ConfigManager.js +531 -0
- package/lib/errors/AIProviderError.js +164 -0
- package/lib/services/AgentService.js +557 -0
- package/lib/services/EpicService.js +609 -0
- package/lib/services/PRDService.js +928 -103
- package/lib/services/TaskService.js +760 -0
- package/lib/services/interfaces.js +753 -0
- package/lib/utils/CircuitBreaker.js +165 -0
- package/lib/utils/Encryption.js +201 -0
- package/lib/utils/RateLimiter.js +241 -0
- package/lib/utils/ServiceFactory.js +165 -0
- package/package.json +6 -5
- package/scripts/config/get.js +108 -0
- package/scripts/config/init.js +100 -0
- package/scripts/config/list-providers.js +93 -0
- package/scripts/config/set-api-key.js +107 -0
- package/scripts/config/set-provider.js +201 -0
- package/scripts/config/set.js +139 -0
- package/scripts/config/show.js +181 -0
- package/autopm/.claude/.env +0 -158
- package/autopm/.claude/settings.local.json +0 -9
|
@@ -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;
|