claude-autopm 2.5.0 → 2.7.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,677 @@
1
+ /**
2
+ * WorkflowService - Project Management and Workflow Service
3
+ *
4
+ * Pure service layer for workflow operations following ClaudeAutoPM patterns.
5
+ * Follows 3-layer architecture: Service (logic) -> No direct I/O
6
+ *
7
+ * Provides comprehensive workflow management:
8
+ *
9
+ * 1. Task Prioritization & Selection (2 methods):
10
+ * - getNextTask: Get next priority task based on dependencies, priorities, status
11
+ * - getWhatNext: AI-powered suggestions for next steps
12
+ *
13
+ * 2. Project Reporting (3 methods):
14
+ * - generateStandup: Generate daily standup report
15
+ * - getProjectStatus: Overall project health and metrics
16
+ * - getInProgressTasks: All currently active tasks
17
+ *
18
+ * 3. Bottleneck Analysis (2 methods):
19
+ * - getBlockedTasks: All blocked tasks with reasons
20
+ * - analyzeBottlenecks: Identify workflow bottlenecks
21
+ *
22
+ * 4. Metrics & Analysis (3 methods):
23
+ * - calculateVelocity: Calculate team/project velocity
24
+ * - prioritizeTasks: Task prioritization logic
25
+ * - resolveDependencies: Check and resolve task dependencies
26
+ *
27
+ * Documentation Queries:
28
+ * - mcp://context7/agile/workflow-management - Agile workflow patterns
29
+ * - mcp://context7/agile/velocity-tracking - Velocity and metrics
30
+ * - mcp://context7/project-management/standup - Daily standup best practices
31
+ * - mcp://context7/project-management/bottlenecks - Bottleneck identification
32
+ */
33
+
34
+ class WorkflowService {
35
+ /**
36
+ * Create a new WorkflowService instance
37
+ *
38
+ * @param {Object} options - Configuration options
39
+ * @param {Object} options.issueService - IssueService instance (required)
40
+ * @param {Object} options.epicService - EpicService instance (required)
41
+ * @param {Object} [options.prdService] - PRDService instance (optional)
42
+ */
43
+ constructor(options = {}) {
44
+ if (!options.issueService) {
45
+ throw new Error('IssueService is required');
46
+ }
47
+ if (!options.epicService) {
48
+ throw new Error('EpicService is required');
49
+ }
50
+
51
+ this.issueService = options.issueService;
52
+ this.epicService = options.epicService;
53
+ this.prdService = options.prdService;
54
+
55
+ // Priority order: P0 (highest) -> P1 -> P2 -> P3 (lowest)
56
+ this.priorityOrder = { 'P0': 0, 'P1': 1, 'P2': 2, 'P3': 3 };
57
+ }
58
+
59
+ // ==========================================
60
+ // 1. TASK PRIORITIZATION & SELECTION
61
+ // ==========================================
62
+
63
+ /**
64
+ * Get next priority task based on dependencies, priorities, status
65
+ *
66
+ * Algorithm:
67
+ * 1. Filter open tasks only
68
+ * 2. Check dependencies (skip if any are open)
69
+ * 3. Sort by priority (P0 > P1 > P2 > P3)
70
+ * 4. Within same priority, oldest first
71
+ * 5. Return top task with reasoning
72
+ *
73
+ * @returns {Promise<Object|null>} Next task with reasoning, or null if none available
74
+ */
75
+ async getNextTask() {
76
+ try {
77
+ // Get all issues
78
+ const allIssues = await this.issueService.listIssues();
79
+ if (!allIssues || allIssues.length === 0) {
80
+ return null;
81
+ }
82
+
83
+ // Filter open tasks
84
+ const openTasks = allIssues.filter(issue => {
85
+ const status = this.issueService.categorizeStatus(issue.status);
86
+ return status === 'open';
87
+ });
88
+
89
+ if (openTasks.length === 0) {
90
+ return null;
91
+ }
92
+
93
+ // Check dependencies for each task
94
+ const availableTasks = [];
95
+ for (const task of openTasks) {
96
+ const deps = await this.resolveDependencies(task.id);
97
+ if (deps.resolved) {
98
+ availableTasks.push(task);
99
+ }
100
+ }
101
+
102
+ if (availableTasks.length === 0) {
103
+ return null;
104
+ }
105
+
106
+ // Sort by priority
107
+ const prioritized = this.prioritizeTasks(availableTasks);
108
+
109
+ // Get first task
110
+ const nextTask = prioritized[0];
111
+
112
+ // Generate reasoning
113
+ const reasoning = this._generateTaskReasoning(nextTask, availableTasks);
114
+
115
+ return {
116
+ id: nextTask.id,
117
+ title: nextTask.title,
118
+ status: nextTask.status,
119
+ priority: nextTask.priority,
120
+ epic: nextTask.epic,
121
+ effort: nextTask.effort,
122
+ reasoning
123
+ };
124
+ } catch (error) {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Generate reasoning for why a task is recommended
131
+ * @private
132
+ */
133
+ _generateTaskReasoning(task, allAvailable) {
134
+ const reasons = [];
135
+
136
+ // Priority reasoning
137
+ const priority = task.priority || 'P2';
138
+ if (priority === 'P0') {
139
+ reasons.push('Highest priority (P0) - critical task');
140
+ } else if (priority === 'P1') {
141
+ reasons.push('High priority (P1) - important task');
142
+ }
143
+
144
+ // Dependencies resolved
145
+ reasons.push('All dependencies completed');
146
+
147
+ // Unblocked
148
+ reasons.push('No blocking issues');
149
+
150
+ // Count of available alternatives
151
+ if (allAvailable.length > 1) {
152
+ reasons.push(`${allAvailable.length - 1} other tasks also available`);
153
+ }
154
+
155
+ return reasons.join('. ') + '.';
156
+ }
157
+
158
+ /**
159
+ * AI-powered suggestions for next steps (what to work on)
160
+ *
161
+ * Analyzes project state and suggests contextual next actions:
162
+ * - No PRDs: Create first PRD
163
+ * - No epics: Parse PRD into epic
164
+ * - No tasks: Decompose epic
165
+ * - Tasks available: Start working
166
+ * - Tasks in progress: Continue work
167
+ *
168
+ * @returns {Promise<Object>} Suggestions with project state
169
+ */
170
+ async getWhatNext() {
171
+ const projectState = await this._analyzeProjectState();
172
+ const suggestions = this._generateSuggestions(projectState);
173
+
174
+ return {
175
+ projectState,
176
+ suggestions
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Analyze current project state
182
+ * @private
183
+ */
184
+ async _analyzeProjectState() {
185
+ const state = {
186
+ prdCount: 0,
187
+ epicCount: 0,
188
+ issueCount: 0,
189
+ openIssues: 0,
190
+ inProgressIssues: 0,
191
+ blockedIssues: 0
192
+ };
193
+
194
+ try {
195
+ // Count PRDs
196
+ if (this.prdService && this.prdService.listPRDs) {
197
+ const prds = await this.prdService.listPRDs();
198
+ state.prdCount = prds.length;
199
+ }
200
+
201
+ // Count epics
202
+ const epics = await this.epicService.listEpics();
203
+ state.epicCount = epics.length;
204
+
205
+ // Count issues
206
+ const issues = await this.issueService.listIssues();
207
+ state.issueCount = issues.length;
208
+ state.openIssues = issues.filter(i => this.issueService.categorizeStatus(i.status) === 'open').length;
209
+ state.inProgressIssues = issues.filter(i => this.issueService.categorizeStatus(i.status) === 'in_progress').length;
210
+ state.blockedIssues = issues.filter(i => (i.status || '').toLowerCase() === 'blocked').length;
211
+ } catch (error) {
212
+ // Errors handled gracefully
213
+ }
214
+
215
+ return state;
216
+ }
217
+
218
+ /**
219
+ * Generate contextual suggestions
220
+ * @private
221
+ */
222
+ _generateSuggestions(state) {
223
+ const suggestions = [];
224
+
225
+ // Scenario 1: No PRDs
226
+ if (state.prdCount === 0) {
227
+ suggestions.push({
228
+ priority: 'high',
229
+ recommended: true,
230
+ title: 'Create Your First PRD',
231
+ description: 'Start by defining what you want to build',
232
+ commands: ['autopm prd create my-feature'],
233
+ why: 'PRDs define requirements and guide development'
234
+ });
235
+ return suggestions;
236
+ }
237
+
238
+ // Scenario 2: No epics
239
+ if (state.epicCount === 0) {
240
+ suggestions.push({
241
+ priority: 'high',
242
+ recommended: true,
243
+ title: 'Parse PRD into Epic',
244
+ description: 'Convert requirements into executable epic',
245
+ commands: ['autopm prd parse <prd-name>'],
246
+ why: 'Creates epic structure needed for task breakdown'
247
+ });
248
+ return suggestions;
249
+ }
250
+
251
+ // Scenario 3: Open tasks available
252
+ if (state.openIssues > 0) {
253
+ suggestions.push({
254
+ priority: 'high',
255
+ recommended: true,
256
+ title: 'Start Working on Tasks',
257
+ description: `You have ${state.openIssues} tasks ready to work on`,
258
+ commands: ['autopm pm next', 'autopm issue start <number>'],
259
+ why: 'Begin implementation with TDD approach'
260
+ });
261
+ }
262
+
263
+ // Scenario 4: Tasks in progress
264
+ if (state.inProgressIssues > 0) {
265
+ suggestions.push({
266
+ priority: 'medium',
267
+ recommended: state.openIssues === 0,
268
+ title: 'Continue In-Progress Work',
269
+ description: `You have ${state.inProgressIssues} tasks currently in progress`,
270
+ commands: ['autopm pm in-progress'],
271
+ why: 'Finish what you started before starting new work'
272
+ });
273
+ }
274
+
275
+ // Scenario 5: Blocked tasks
276
+ if (state.blockedIssues > 0) {
277
+ suggestions.push({
278
+ priority: 'high',
279
+ recommended: true,
280
+ title: 'Unblock Tasks',
281
+ description: `${state.blockedIssues} tasks are blocked`,
282
+ commands: ['autopm pm blocked'],
283
+ why: 'Blocked tasks prevent progress'
284
+ });
285
+ }
286
+
287
+ return suggestions;
288
+ }
289
+
290
+ // ==========================================
291
+ // 2. PROJECT REPORTING
292
+ // ==========================================
293
+
294
+ /**
295
+ * Generate daily standup report
296
+ *
297
+ * Includes:
298
+ * - Yesterday: tasks closed in last 24h
299
+ * - Today: tasks currently in-progress
300
+ * - Blockers: tasks blocked and why
301
+ * - Velocity: recent completion rate
302
+ * - Sprint progress: overall completion
303
+ *
304
+ * @returns {Promise<Object>} Standup report data
305
+ */
306
+ async generateStandup() {
307
+ const now = new Date();
308
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
309
+
310
+ const report = {
311
+ date: now.toISOString().split('T')[0],
312
+ yesterday: [],
313
+ today: [],
314
+ blockers: [],
315
+ velocity: 0,
316
+ sprintProgress: {
317
+ completed: 0,
318
+ total: 0,
319
+ percentage: 0
320
+ }
321
+ };
322
+
323
+ try {
324
+ // Yesterday: completed tasks in last 24h
325
+ const allIssues = await this.issueService.listIssues();
326
+ const yesterdayTasks = allIssues.filter(issue => {
327
+ if (!issue.completed) return false;
328
+ const completedDate = new Date(issue.completed);
329
+ return completedDate >= yesterday;
330
+ });
331
+ report.yesterday = yesterdayTasks;
332
+
333
+ // Today: in-progress tasks
334
+ const inProgressTasks = await this.getInProgressTasks();
335
+ report.today = inProgressTasks;
336
+
337
+ // Blockers: blocked tasks
338
+ const blockedTasks = await this.getBlockedTasks();
339
+ report.blockers = blockedTasks;
340
+
341
+ // Velocity: 7-day average
342
+ report.velocity = await this.calculateVelocity(7);
343
+
344
+ // Sprint progress
345
+ const closedCount = allIssues.filter(i => this.issueService.categorizeStatus(i.status) === 'closed').length;
346
+ report.sprintProgress = {
347
+ completed: closedCount,
348
+ total: allIssues.length,
349
+ percentage: allIssues.length > 0 ? Math.round((closedCount * 100) / allIssues.length) : 0
350
+ };
351
+ } catch (error) {
352
+ // Errors handled gracefully
353
+ }
354
+
355
+ return report;
356
+ }
357
+
358
+ /**
359
+ * Overall project health and metrics
360
+ *
361
+ * @returns {Promise<Object>} Project status with health indicators
362
+ */
363
+ async getProjectStatus() {
364
+ const status = {
365
+ epics: {
366
+ backlog: 0,
367
+ planning: 0,
368
+ inProgress: 0,
369
+ completed: 0,
370
+ total: 0
371
+ },
372
+ issues: {
373
+ open: 0,
374
+ inProgress: 0,
375
+ blocked: 0,
376
+ closed: 0,
377
+ total: 0
378
+ },
379
+ progress: {
380
+ overall: 0,
381
+ velocity: 0
382
+ },
383
+ health: 'ON_TRACK',
384
+ recommendations: []
385
+ };
386
+
387
+ try {
388
+ // Epics
389
+ const epics = await this.epicService.listEpics();
390
+ status.epics.total = epics.length;
391
+ epics.forEach(epic => {
392
+ const category = this.epicService.categorizeStatus(epic.status);
393
+ if (category === 'backlog') status.epics.backlog++;
394
+ else if (category === 'planning') status.epics.planning++;
395
+ else if (category === 'in_progress') status.epics.inProgress++;
396
+ else if (category === 'done') status.epics.completed++;
397
+ });
398
+
399
+ // Issues
400
+ const issues = await this.issueService.listIssues();
401
+ status.issues.total = issues.length;
402
+ issues.forEach(issue => {
403
+ const category = this.issueService.categorizeStatus(issue.status);
404
+ if (category === 'open') status.issues.open++;
405
+ else if (category === 'in_progress') status.issues.inProgress++;
406
+ else if (category === 'closed') status.issues.closed++;
407
+
408
+ if ((issue.status || '').toLowerCase() === 'blocked') {
409
+ status.issues.blocked++;
410
+ }
411
+ });
412
+
413
+ // Progress
414
+ if (status.issues.total > 0) {
415
+ status.progress.overall = Math.round((status.issues.closed * 100) / status.issues.total);
416
+ }
417
+ status.progress.velocity = await this.calculateVelocity(7);
418
+
419
+ // Health check
420
+ const { health, recommendations } = this._assessProjectHealth(status);
421
+ status.health = health;
422
+ status.recommendations = recommendations;
423
+ } catch (error) {
424
+ // Errors handled gracefully
425
+ }
426
+
427
+ return status;
428
+ }
429
+
430
+ /**
431
+ * Assess project health based on metrics
432
+ * @private
433
+ */
434
+ _assessProjectHealth(status) {
435
+ let health = 'ON_TRACK';
436
+ const recommendations = [];
437
+
438
+ // Check for blocked tasks >2 days
439
+ if (status.issues.blocked >= 2) {
440
+ health = 'AT_RISK';
441
+ recommendations.push('Unblock multiple blocked tasks preventing progress');
442
+ }
443
+
444
+ // Check for no velocity (only if we have tasks)
445
+ if (status.progress.velocity === 0 && status.issues.closed === 0 && status.issues.total > 0) {
446
+ health = 'AT_RISK';
447
+ recommendations.push('No tasks completed recently - team may be blocked');
448
+ }
449
+
450
+ // Check for high WIP
451
+ if (status.issues.inProgress > status.issues.total * 0.5 && status.issues.total >= 5) {
452
+ recommendations.push('High work-in-progress ratio - consider finishing tasks before starting new ones');
453
+ }
454
+
455
+ return { health, recommendations };
456
+ }
457
+
458
+ /**
459
+ * Get all currently active tasks across epics
460
+ *
461
+ * @returns {Promise<Array>} Array of in-progress tasks with metadata
462
+ */
463
+ async getInProgressTasks() {
464
+ try {
465
+ const allIssues = await this.issueService.listIssues();
466
+ const inProgress = allIssues.filter(issue => {
467
+ const category = this.issueService.categorizeStatus(issue.status);
468
+ return category === 'in_progress';
469
+ });
470
+
471
+ // Mark stale tasks (>3 days)
472
+ const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000;
473
+ return inProgress.map(task => {
474
+ const started = task.started ? new Date(task.started).getTime() : Date.now();
475
+ const stale = started < threeDaysAgo;
476
+
477
+ return {
478
+ ...task,
479
+ stale
480
+ };
481
+ });
482
+ } catch (error) {
483
+ return [];
484
+ }
485
+ }
486
+
487
+ // ==========================================
488
+ // 3. BOTTLENECK ANALYSIS
489
+ // ==========================================
490
+
491
+ /**
492
+ * Get all blocked tasks with reasons
493
+ *
494
+ * @returns {Promise<Array>} Array of blocked tasks with analysis
495
+ */
496
+ async getBlockedTasks() {
497
+ try {
498
+ const allIssues = await this.issueService.listIssues();
499
+ const blocked = allIssues.filter(issue => {
500
+ return (issue.status || '').toLowerCase() === 'blocked';
501
+ });
502
+
503
+ return blocked.map(task => {
504
+ // Calculate days blocked
505
+ const blockedSince = task.blocked_since ? new Date(task.blocked_since) : new Date();
506
+ const daysBlocked = Math.floor((Date.now() - blockedSince.getTime()) / (24 * 60 * 60 * 1000));
507
+
508
+ // Suggest action
509
+ let suggestedAction = 'Review blocking dependencies';
510
+ if (task.dependencies && task.dependencies.length > 0) {
511
+ suggestedAction = `Complete dependencies: ${task.dependencies.join(', ')}`;
512
+ }
513
+
514
+ return {
515
+ id: task.id,
516
+ title: task.title,
517
+ reason: task.blocked_reason || 'Unknown',
518
+ daysBlocked,
519
+ suggestedAction
520
+ };
521
+ });
522
+ } catch (error) {
523
+ return [];
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Identify workflow bottlenecks
529
+ *
530
+ * Analyzes:
531
+ * - Blocked tasks
532
+ * - Stale in-progress tasks
533
+ * - High WIP ratio
534
+ *
535
+ * @returns {Promise<Array>} Array of bottleneck objects
536
+ */
537
+ async analyzeBottlenecks() {
538
+ const bottlenecks = [];
539
+
540
+ try {
541
+ const allIssues = await this.issueService.listIssues();
542
+
543
+ // Check for blocked tasks
544
+ const blocked = allIssues.filter(i => (i.status || '').toLowerCase() === 'blocked');
545
+ if (blocked.length > 0) {
546
+ bottlenecks.push({
547
+ type: 'BLOCKED_TASKS',
548
+ count: blocked.length,
549
+ severity: blocked.length >= 3 ? 'HIGH' : 'MEDIUM',
550
+ description: `${blocked.length} tasks are blocked`
551
+ });
552
+ }
553
+
554
+ // Check for stale in-progress tasks
555
+ const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000;
556
+ const stale = allIssues.filter(issue => {
557
+ const category = this.issueService.categorizeStatus(issue.status);
558
+ if (category !== 'in_progress') return false;
559
+ const started = issue.started ? new Date(issue.started).getTime() : Date.now();
560
+ return started < threeDaysAgo;
561
+ });
562
+
563
+ if (stale.length > 0) {
564
+ bottlenecks.push({
565
+ type: 'STALE_TASKS',
566
+ count: stale.length,
567
+ severity: 'MEDIUM',
568
+ description: `${stale.length} tasks in progress for >3 days`
569
+ });
570
+ }
571
+ } catch (error) {
572
+ // Errors handled gracefully
573
+ }
574
+
575
+ return bottlenecks;
576
+ }
577
+
578
+ // ==========================================
579
+ // 4. METRICS & ANALYSIS
580
+ // ==========================================
581
+
582
+ /**
583
+ * Calculate team/project velocity
584
+ *
585
+ * Velocity = tasks completed / time period (days)
586
+ *
587
+ * @param {number} days - Number of days to calculate over (default: 7)
588
+ * @returns {Promise<number>} Tasks per day
589
+ */
590
+ async calculateVelocity(days = 7) {
591
+ try {
592
+ const allIssues = await this.issueService.listIssues();
593
+ const cutoffDate = Date.now() - days * 24 * 60 * 60 * 1000;
594
+
595
+ const recentCompletions = allIssues.filter(issue => {
596
+ if (!issue.completed) return false;
597
+ const completedDate = new Date(issue.completed).getTime();
598
+ return completedDate >= cutoffDate;
599
+ });
600
+
601
+ const velocity = recentCompletions.length / days;
602
+ return Math.round(velocity * 10) / 10; // Round to 1 decimal
603
+ } catch (error) {
604
+ return 0;
605
+ }
606
+ }
607
+
608
+ /**
609
+ * Task prioritization logic
610
+ *
611
+ * Sorts by:
612
+ * 1. Priority (P0 > P1 > P2 > P3)
613
+ * 2. Creation date (oldest first within same priority)
614
+ *
615
+ * @param {Array} tasks - Array of task objects
616
+ * @returns {Array} Sorted tasks
617
+ */
618
+ prioritizeTasks(tasks) {
619
+ if (!Array.isArray(tasks)) {
620
+ return [];
621
+ }
622
+
623
+ return [...tasks].sort((a, b) => {
624
+ // Priority first
625
+ const aPriority = this.priorityOrder[a.priority] ?? this.priorityOrder['P2'];
626
+ const bPriority = this.priorityOrder[b.priority] ?? this.priorityOrder['P2'];
627
+
628
+ if (aPriority !== bPriority) {
629
+ return aPriority - bPriority;
630
+ }
631
+
632
+ // Creation date second (older first)
633
+ const aCreated = a.created ? new Date(a.created).getTime() : 0;
634
+ const bCreated = b.created ? new Date(b.created).getTime() : 0;
635
+ return aCreated - bCreated;
636
+ });
637
+ }
638
+
639
+ /**
640
+ * Check and resolve task dependencies
641
+ *
642
+ * @param {string|number} issueNumber - Issue number to check
643
+ * @returns {Promise<Object>} { resolved: boolean, blocking: Array }
644
+ */
645
+ async resolveDependencies(issueNumber) {
646
+ try {
647
+ const dependencies = await this.issueService.getDependencies(issueNumber);
648
+
649
+ if (!dependencies || dependencies.length === 0) {
650
+ return { resolved: true, blocking: [] };
651
+ }
652
+
653
+ const blocking = [];
654
+ for (const depId of dependencies) {
655
+ try {
656
+ const dep = await this.issueService.getLocalIssue(depId);
657
+ const category = this.issueService.categorizeStatus(dep.status);
658
+ if (category !== 'closed') {
659
+ blocking.push(depId);
660
+ }
661
+ } catch (error) {
662
+ // If we can't read dependency, consider it blocking
663
+ blocking.push(depId);
664
+ }
665
+ }
666
+
667
+ return {
668
+ resolved: blocking.length === 0,
669
+ blocking
670
+ };
671
+ } catch (error) {
672
+ return { resolved: true, blocking: [] };
673
+ }
674
+ }
675
+ }
676
+
677
+ module.exports = WorkflowService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-autopm",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Autonomous Project Management Framework for Claude Code - Advanced AI-powered development automation",
5
5
  "main": "bin/autopm.js",
6
6
  "bin": {