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.
@@ -0,0 +1,609 @@
1
+ /**
2
+ * EpicService - Epic Management Service
3
+ *
4
+ * Pure service layer for epic operations without any I/O operations.
5
+ * Follows 3-layer architecture: Service (logic) -> No direct I/O
6
+ *
7
+ * Provides 12 pure business logic methods:
8
+ *
9
+ * 1. Status & Categorization (5 methods):
10
+ * - categorizeStatus: Categorize epic status
11
+ * - isTaskClosed: Check if task is completed
12
+ * - calculateProgress: Calculate completion percentage
13
+ * - generateProgressBar: Generate visual progress bar
14
+ * - hasValidDependencies: Validate dependency format
15
+ *
16
+ * 2. GitHub Integration (2 methods):
17
+ * - extractGitHubIssue: Extract issue number from URL
18
+ * - formatGitHubUrl: Build GitHub URL
19
+ *
20
+ * 3. Content Analysis (3 methods):
21
+ * - analyzePRD: Analyze PRD using PRDService
22
+ * - determineDependencies: Determine feature dependencies
23
+ * - generateEpicMetadata: Generate epic frontmatter
24
+ *
25
+ * 4. Content Generation (2 methods):
26
+ * - generateEpicContent: Build complete epic markdown
27
+ * - buildTaskSection: Format tasks as markdown list
28
+ *
29
+ * Documentation Queries:
30
+ * - mcp://context7/agile/epic-management - Epic management best practices
31
+ * - mcp://context7/agile/task-breakdown - Task breakdown patterns
32
+ * - mcp://context7/project-management/dependencies - Dependency management
33
+ * - mcp://context7/markdown/frontmatter - YAML frontmatter patterns
34
+ */
35
+
36
+ const PRDService = require('./PRDService');
37
+
38
+ class EpicService {
39
+ /**
40
+ * Create a new EpicService instance
41
+ *
42
+ * @param {Object} options - Configuration options
43
+ * @param {PRDService} options.prdService - PRDService instance for parsing
44
+ * @param {ConfigManager} [options.configManager] - Optional ConfigManager instance
45
+ * @param {Object} [options.provider] - Optional AI provider instance for streaming
46
+ */
47
+ constructor(options = {}) {
48
+ if (!options.prdService) {
49
+ throw new Error('PRDService instance is required');
50
+ }
51
+
52
+ if (!(options.prdService instanceof PRDService)) {
53
+ throw new Error('prdService must be an instance of PRDService');
54
+ }
55
+
56
+ this.prdService = options.prdService;
57
+
58
+ // Store ConfigManager if provided (for future use)
59
+ this.configManager = options.configManager || undefined;
60
+
61
+ // Store provider if provided (for streaming operations)
62
+ this.provider = options.provider || undefined;
63
+ }
64
+
65
+ // ==========================================
66
+ // 1. STATUS & CATEGORIZATION (5 METHODS)
67
+ // ==========================================
68
+
69
+ /**
70
+ * Categorize epic status into standard buckets
71
+ *
72
+ * Maps various status strings to standardized categories:
73
+ * - backlog: Not started, awaiting prioritization
74
+ * - planning: Planning/draft phase
75
+ * - in_progress: Active development
76
+ * - done: Completed/closed
77
+ *
78
+ * @param {string} status - Raw status string
79
+ * @returns {string} Categorized status (backlog|planning|in_progress|done)
80
+ *
81
+ * @example
82
+ * categorizeStatus('in-progress') // Returns 'in_progress'
83
+ * categorizeStatus('completed') // Returns 'done'
84
+ * categorizeStatus('unknown') // Returns 'planning' (default)
85
+ */
86
+ categorizeStatus(status) {
87
+ const lowerStatus = (status || '').toLowerCase();
88
+
89
+ // Backlog statuses
90
+ if (lowerStatus === 'backlog') {
91
+ return 'backlog';
92
+ }
93
+
94
+ // Planning statuses
95
+ if (['planning', 'draft', ''].includes(lowerStatus)) {
96
+ return 'planning';
97
+ }
98
+
99
+ // In-progress statuses
100
+ if (['in-progress', 'in_progress', 'active', 'started'].includes(lowerStatus)) {
101
+ return 'in_progress';
102
+ }
103
+
104
+ // Completed statuses
105
+ if (['completed', 'complete', 'done', 'closed', 'finished'].includes(lowerStatus)) {
106
+ return 'done';
107
+ }
108
+
109
+ // Default to planning for unknown statuses
110
+ return 'planning';
111
+ }
112
+
113
+ /**
114
+ * Check if task is in closed/completed state
115
+ *
116
+ * @param {Object} task - Task object with status field
117
+ * @returns {boolean} True if task is closed/completed
118
+ *
119
+ * @example
120
+ * isTaskClosed({ status: 'closed' }) // Returns true
121
+ * isTaskClosed({ status: 'open' }) // Returns false
122
+ */
123
+ isTaskClosed(task) {
124
+ const status = (task?.status || '').toLowerCase();
125
+ return ['closed', 'completed'].includes(status);
126
+ }
127
+
128
+ /**
129
+ * Calculate progress percentage from task array
130
+ *
131
+ * Calculates completion percentage based on closed vs total tasks.
132
+ * Returns 0 for empty or null arrays.
133
+ *
134
+ * @param {Array<Object>} tasks - Array of task objects with status
135
+ * @returns {number} Progress percentage (0-100), rounded to nearest integer
136
+ *
137
+ * @example
138
+ * calculateProgress([
139
+ * { status: 'closed' },
140
+ * { status: 'open' }
141
+ * ]) // Returns 50
142
+ */
143
+ calculateProgress(tasks) {
144
+ if (!Array.isArray(tasks) || tasks.length === 0) {
145
+ return 0;
146
+ }
147
+
148
+ const closedCount = tasks.filter(task => this.isTaskClosed(task)).length;
149
+ const percent = Math.round((closedCount * 100) / tasks.length);
150
+
151
+ return percent;
152
+ }
153
+
154
+ /**
155
+ * Generate visual progress bar
156
+ *
157
+ * Creates ASCII progress bar with filled/empty characters.
158
+ *
159
+ * @param {number} percent - Progress percentage (0-100)
160
+ * @param {number} totalChars - Total bar length in characters (default: 20)
161
+ * @returns {Object} Progress bar data:
162
+ * - bar: String representation of progress bar
163
+ * - percent: Input percentage
164
+ * - filled: Number of filled characters
165
+ * - empty: Number of empty characters
166
+ *
167
+ * @example
168
+ * generateProgressBar(50, 20)
169
+ * // Returns: {
170
+ * // bar: '[██████████░░░░░░░░░░]',
171
+ * // percent: 50,
172
+ * // filled: 10,
173
+ * // empty: 10
174
+ * // }
175
+ */
176
+ generateProgressBar(percent, totalChars = 20) {
177
+ const filled = Math.round((percent * totalChars) / 100);
178
+ const empty = totalChars - filled;
179
+
180
+ let bar = '[';
181
+ bar += '█'.repeat(filled);
182
+ bar += '░'.repeat(empty);
183
+ bar += ']';
184
+
185
+ return {
186
+ bar,
187
+ percent,
188
+ filled,
189
+ empty
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Validate if dependency string has valid content
195
+ *
196
+ * Checks if dependency string contains actual dependency data
197
+ * after cleaning up formatting (brackets, whitespace, etc).
198
+ *
199
+ * @param {string} dependencies - Dependency string to validate
200
+ * @returns {boolean} True if dependencies are present and valid
201
+ *
202
+ * @example
203
+ * hasValidDependencies('epic-123') // Returns true
204
+ * hasValidDependencies('[epic-1, epic-2]') // Returns true
205
+ * hasValidDependencies('[]') // Returns false
206
+ * hasValidDependencies('depends_on:') // Returns false
207
+ */
208
+ hasValidDependencies(dependencies) {
209
+ if (!dependencies || typeof dependencies !== 'string') {
210
+ return false;
211
+ }
212
+
213
+ // Handle malformed dependency strings
214
+ if (dependencies === 'depends_on:') {
215
+ return false;
216
+ }
217
+
218
+ // Clean up the dependency string
219
+ let cleanDeps = dependencies.trim();
220
+
221
+ // Remove array brackets if present
222
+ cleanDeps = cleanDeps.replace(/^\[|\]$/g, '');
223
+
224
+ // Check if there's actual content after cleaning
225
+ cleanDeps = cleanDeps.trim();
226
+
227
+ return cleanDeps.length > 0;
228
+ }
229
+
230
+ // ==========================================
231
+ // 2. GITHUB INTEGRATION (2 METHODS)
232
+ // ==========================================
233
+
234
+ /**
235
+ * Extract GitHub issue number from URL
236
+ *
237
+ * Extracts numeric issue/PR number from GitHub URL.
238
+ * Supports both issues and pull requests.
239
+ *
240
+ * @param {string} githubUrl - GitHub issue or PR URL
241
+ * @returns {string|null} Issue number or null if not found
242
+ *
243
+ * @example
244
+ * extractGitHubIssue('https://github.com/user/repo/issues/123')
245
+ * // Returns '123'
246
+ *
247
+ * extractGitHubIssue('https://github.com/user/repo/pull/456')
248
+ * // Returns '456'
249
+ */
250
+ extractGitHubIssue(githubUrl) {
251
+ if (!githubUrl) {
252
+ return null;
253
+ }
254
+
255
+ // Match issue/PR number at end of URL (before optional trailing slash or query params)
256
+ const match = githubUrl.match(/\/(\d+)(?:\/|\?|$)/);
257
+ return match ? match[1] : null;
258
+ }
259
+
260
+ /**
261
+ * Format GitHub issue URL from components
262
+ *
263
+ * Builds standard GitHub issue URL from owner, repo, and issue number.
264
+ *
265
+ * @param {string} repoOwner - GitHub repository owner
266
+ * @param {string} repoName - GitHub repository name
267
+ * @param {number|string} issueNumber - Issue number
268
+ * @returns {string} Formatted GitHub URL
269
+ * @throws {Error} If required parameters are missing
270
+ *
271
+ * @example
272
+ * formatGitHubUrl('user', 'repo', 123)
273
+ * // Returns 'https://github.com/user/repo/issues/123'
274
+ */
275
+ formatGitHubUrl(repoOwner, repoName, issueNumber) {
276
+ if (!repoOwner || repoOwner.trim() === '') {
277
+ throw new Error('Repository owner is required');
278
+ }
279
+
280
+ if (!repoName || repoName.trim() === '') {
281
+ throw new Error('Repository name is required');
282
+ }
283
+
284
+ if (!issueNumber) {
285
+ throw new Error('Issue number is required');
286
+ }
287
+
288
+ return `https://github.com/${repoOwner}/${repoName}/issues/${issueNumber}`;
289
+ }
290
+
291
+ // ==========================================
292
+ // 3. CONTENT ANALYSIS (3 METHODS)
293
+ // ==========================================
294
+
295
+ /**
296
+ * Analyze PRD content using PRDService
297
+ *
298
+ * Parses PRD markdown to extract frontmatter and sections.
299
+ * Uses injected PRDService for parsing logic.
300
+ *
301
+ * @param {string} prdContent - PRD markdown content
302
+ * @returns {Object} Analysis result:
303
+ * - frontmatter: Parsed YAML frontmatter
304
+ * - sections: Extracted PRD sections (vision, features, etc)
305
+ *
306
+ * @example
307
+ * analyzePRD(prdMarkdown)
308
+ * // Returns:
309
+ * // {
310
+ * // frontmatter: { title: 'Feature', priority: 'P1' },
311
+ * // sections: { vision: '...', features: [...] }
312
+ * // }
313
+ */
314
+ analyzePRD(prdContent) {
315
+ const frontmatter = this.prdService.parseFrontmatter(prdContent);
316
+ const sections = this.prdService.extractPrdContent(prdContent);
317
+
318
+ return {
319
+ frontmatter,
320
+ sections
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Determine dependencies between features
326
+ *
327
+ * Analyzes feature types to determine natural dependencies:
328
+ * - Frontend depends on Backend
329
+ * - Backend depends on Data
330
+ * - Integration depends on all others
331
+ *
332
+ * @param {Array<Object>} features - Array of feature objects with type
333
+ * @returns {Object} Dependency map: { featureName: [dependency1, dependency2] }
334
+ *
335
+ * @example
336
+ * determineDependencies([
337
+ * { name: 'UI', type: 'frontend' },
338
+ * { name: 'API', type: 'backend' }
339
+ * ])
340
+ * // Returns: { 'UI': ['API'] }
341
+ */
342
+ determineDependencies(features) {
343
+ if (!Array.isArray(features) || features.length === 0) {
344
+ return {};
345
+ }
346
+
347
+ const dependencies = {};
348
+
349
+ // Find features by type
350
+ const backendFeatures = features.filter(f => f.type === 'backend');
351
+ const dataFeatures = features.filter(f => f.type === 'data');
352
+ const frontendFeatures = features.filter(f => f.type === 'frontend');
353
+
354
+ // Frontend depends on Backend
355
+ frontendFeatures.forEach(frontend => {
356
+ if (backendFeatures.length > 0) {
357
+ dependencies[frontend.name] = backendFeatures.map(b => b.name);
358
+ }
359
+ });
360
+
361
+ // Backend depends on Data
362
+ backendFeatures.forEach(backend => {
363
+ if (dataFeatures.length > 0) {
364
+ dependencies[backend.name] = dataFeatures.map(d => d.name);
365
+ }
366
+ });
367
+
368
+ return dependencies;
369
+ }
370
+
371
+ /**
372
+ * Generate epic metadata (frontmatter)
373
+ *
374
+ * Creates standardized epic frontmatter with required and optional fields.
375
+ *
376
+ * @param {string} name - Epic name
377
+ * @param {string} prdId - PRD identifier
378
+ * @param {Object} options - Optional metadata overrides
379
+ * @param {string} options.status - Epic status (default: 'backlog')
380
+ * @param {string} options.priority - Priority level (default: 'P2')
381
+ * @returns {Object} Epic metadata object
382
+ * @throws {Error} If required parameters are missing
383
+ *
384
+ * @example
385
+ * generateEpicMetadata('user-auth', 'prd-123', { priority: 'P1' })
386
+ * // Returns:
387
+ * // {
388
+ * // name: 'user-auth',
389
+ * // status: 'backlog',
390
+ * // prd_id: 'prd-123',
391
+ * // priority: 'P1',
392
+ * // created: '2025-01-01T00:00:00.000Z',
393
+ * // progress: '0%',
394
+ * // prd: '.claude/prds/user-auth.md',
395
+ * // github: '[Will be updated when synced to GitHub]'
396
+ * // }
397
+ */
398
+ generateEpicMetadata(name, prdId, options = {}) {
399
+ if (!name || name.trim() === '') {
400
+ throw new Error('Epic name is required');
401
+ }
402
+
403
+ if (!prdId || prdId.trim() === '') {
404
+ throw new Error('PRD ID is required');
405
+ }
406
+
407
+ const now = new Date().toISOString();
408
+
409
+ return {
410
+ name,
411
+ status: options.status || 'backlog',
412
+ prd_id: prdId,
413
+ priority: options.priority || 'P2',
414
+ created: now,
415
+ progress: '0%',
416
+ prd: `.claude/prds/${name}.md`,
417
+ github: '[Will be updated when synced to GitHub]'
418
+ };
419
+ }
420
+
421
+ // ==========================================
422
+ // 4. CONTENT GENERATION (2 METHODS)
423
+ // ==========================================
424
+
425
+ /**
426
+ * Generate complete epic markdown content
427
+ *
428
+ * Builds full epic document with frontmatter, sections, and tasks.
429
+ * Follows standard epic template format.
430
+ *
431
+ * @param {Object} metadata - Epic metadata (frontmatter)
432
+ * @param {Object} sections - PRD sections (vision, problem, features, etc)
433
+ * @param {Array<Object>} tasks - Array of task objects
434
+ * @returns {string} Complete epic markdown content
435
+ *
436
+ * @example
437
+ * generateEpicContent(metadata, sections, tasks)
438
+ * // Returns multiline markdown string with:
439
+ * // - YAML frontmatter
440
+ * // - Epic title and overview
441
+ * // - Vision and other sections
442
+ * // - Task breakdown
443
+ */
444
+ generateEpicContent(metadata, sections, tasks) {
445
+ // Build frontmatter
446
+ const frontmatter = `---
447
+ name: ${metadata.name}
448
+ status: ${metadata.status}
449
+ created: ${metadata.created}
450
+ progress: ${metadata.progress}
451
+ prd: ${metadata.prd}
452
+ github: ${metadata.github}
453
+ priority: ${metadata.priority}
454
+ ---`;
455
+
456
+ // Build overview section
457
+ let content = frontmatter + '\n\n';
458
+ content += `# Epic: ${metadata.name}\n\n`;
459
+ content += '## Overview\n';
460
+
461
+ if (sections.vision) {
462
+ content += sections.vision + '\n\n';
463
+ content += `### Vision\n${sections.vision}\n\n`;
464
+ }
465
+
466
+ if (sections.problem) {
467
+ content += `### Problem\n${sections.problem}\n\n`;
468
+ }
469
+
470
+ // Build task breakdown
471
+ content += '## Task Breakdown\n\n';
472
+ const taskSection = this.buildTaskSection(tasks);
473
+ content += taskSection;
474
+
475
+ return content;
476
+ }
477
+
478
+ /**
479
+ * Build task section markdown
480
+ *
481
+ * Formats task array as markdown list with details.
482
+ * Each task includes: ID, title, type, effort, status.
483
+ *
484
+ * @param {Array<Object>} tasks - Array of task objects
485
+ * @returns {string} Markdown formatted task list
486
+ *
487
+ * @example
488
+ * buildTaskSection([
489
+ * { id: 'TASK-1', title: 'Setup', type: 'setup', effort: '2h', status: 'open' }
490
+ * ])
491
+ * // Returns:
492
+ * // ### TASK-1: Setup
493
+ * // - **Type**: setup
494
+ * // - **Effort**: 2h
495
+ * // - **Status**: open
496
+ */
497
+ buildTaskSection(tasks) {
498
+ if (!Array.isArray(tasks) || tasks.length === 0) {
499
+ return '';
500
+ }
501
+
502
+ return tasks.map(task => {
503
+ const status = task.status || 'Not Started';
504
+ return `### ${task.id}: ${task.title}
505
+ - **Type**: ${task.type}
506
+ - **Effort**: ${task.effort}
507
+ - **Status**: ${status}`;
508
+ }).join('\n\n');
509
+ }
510
+
511
+ // ==========================================
512
+ // 5. AI STREAMING METHODS
513
+ // ==========================================
514
+
515
+ /**
516
+ * Decompose epic into tasks with streaming output
517
+ *
518
+ * Streams AI-powered decomposition of epic content into discrete tasks.
519
+ * The AI analyzes the epic and generates task breakdown with estimates,
520
+ * dependencies, and assignments.
521
+ *
522
+ * @param {string} epicContent - Epic markdown content
523
+ * @param {Object} [options] - Streaming options (passed to provider)
524
+ * @returns {AsyncGenerator<string>} Stream of task decomposition text chunks
525
+ * @throws {Error} If provider is not available or lacks stream() support
526
+ *
527
+ * @example
528
+ * for await (const chunk of service.decomposeStream(epicContent)) {
529
+ * process.stdout.write(chunk); // Display task generation progress
530
+ * }
531
+ */
532
+ async *decomposeStream(epicContent, options = {}) {
533
+ if (!this.provider || !this.provider.stream) {
534
+ throw new Error('Streaming requires an AI provider with stream() support');
535
+ }
536
+
537
+ const prompt = `Decompose this epic into specific, actionable tasks.
538
+
539
+ For each task, provide:
540
+ 1. Task ID and title
541
+ 2. Task type (frontend/backend/data/testing/documentation)
542
+ 3. Detailed description
543
+ 4. Effort estimate (in hours or days)
544
+ 5. Dependencies on other tasks
545
+ 6. Acceptance criteria (bullet points)
546
+
547
+ Generate 5-15 tasks that fully cover the epic scope. Tasks should be:
548
+ - Small enough to complete in 1-3 days
549
+ - Independent where possible
550
+ - Clearly defined with acceptance criteria
551
+ - Properly sequenced with dependencies
552
+
553
+ Epic Content:
554
+ ${epicContent}`;
555
+
556
+ for await (const chunk of this.provider.stream(prompt, options)) {
557
+ yield chunk;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Analyze PRD with streaming output
563
+ *
564
+ * Streams AI-powered analysis of PRD content to identify epics, themes,
565
+ * and high-level task breakdown. This is typically used before epic creation
566
+ * to understand the PRD structure and complexity.
567
+ *
568
+ * @param {string} prdContent - PRD markdown content
569
+ * @param {Object} [options] - Streaming options (passed to provider)
570
+ * @returns {AsyncGenerator<string>} Stream of PRD analysis text chunks
571
+ * @throws {Error} If provider is not available or lacks stream() support
572
+ *
573
+ * @example
574
+ * for await (const chunk of service.analyzeStream(prdContent)) {
575
+ * process.stdout.write(chunk); // Display PRD analysis progress
576
+ * }
577
+ */
578
+ async *analyzeStream(prdContent, options = {}) {
579
+ if (!this.provider || !this.provider.stream) {
580
+ throw new Error('Streaming requires an AI provider with stream() support');
581
+ }
582
+
583
+ const prompt = `Analyze this Product Requirements Document and provide a comprehensive epic-level breakdown.
584
+
585
+ Identify:
586
+ 1. Major themes or feature areas (2-5 epics)
587
+ 2. For each potential epic:
588
+ - Epic name and scope
589
+ - Key features/capabilities
590
+ - Estimated complexity (Small/Medium/Large)
591
+ - Dependencies on other epics
592
+ - Rough task count estimate
593
+
594
+ Also provide:
595
+ - Overall project complexity assessment
596
+ - Recommended epic breakdown approach
597
+ - Key technical risks or challenges
598
+ - Suggested development sequence
599
+
600
+ PRD Content:
601
+ ${prdContent}`;
602
+
603
+ for await (const chunk of this.provider.stream(prompt, options)) {
604
+ yield chunk;
605
+ }
606
+ }
607
+ }
608
+
609
+ module.exports = EpicService;