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.
- package/bin/autopm.js +4 -0
- package/lib/cli/commands/context.js +477 -0
- package/lib/cli/commands/pm.js +827 -0
- package/lib/services/ContextService.js +595 -0
- package/lib/services/UtilityService.js +847 -0
- package/lib/services/WorkflowService.js +677 -0
- package/package.json +1 -1
|
@@ -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;
|