claude-autopm 2.5.0 → 2.6.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 +2 -0
- package/lib/cli/commands/pm.js +528 -0
- package/lib/services/WorkflowService.js +677 -0
- package/package.json +1 -1
package/bin/autopm.js
CHANGED
|
@@ -184,6 +184,8 @@ function main() {
|
|
|
184
184
|
.command(require('../lib/cli/commands/epic'))
|
|
185
185
|
// Issue management command (STANDALONE)
|
|
186
186
|
.command(require('../lib/cli/commands/issue'))
|
|
187
|
+
// PM workflow commands (STANDALONE)
|
|
188
|
+
.command(require('../lib/cli/commands/pm'))
|
|
187
189
|
// PRD management command (STANDALONE)
|
|
188
190
|
.command(require('../lib/cli/commands/prd'))
|
|
189
191
|
// Task management command (STANDALONE)
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI PM (Project Management) Commands
|
|
3
|
+
*
|
|
4
|
+
* Provides workflow and project management commands for ClaudeAutoPM.
|
|
5
|
+
* Implements subcommands for workflow analysis and task prioritization.
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* - next: Get next priority task based on dependencies and priorities
|
|
9
|
+
* - what-next: AI-powered suggestions for next steps
|
|
10
|
+
* - standup: Generate daily standup report
|
|
11
|
+
* - status: Project status overview and health
|
|
12
|
+
* - in-progress: Show all active tasks
|
|
13
|
+
* - blocked: Show all blocked tasks
|
|
14
|
+
*
|
|
15
|
+
* @module cli/commands/pm
|
|
16
|
+
* @requires ../../services/WorkflowService
|
|
17
|
+
* @requires ../../services/IssueService
|
|
18
|
+
* @requires ../../services/EpicService
|
|
19
|
+
* @requires fs-extra
|
|
20
|
+
* @requires ora
|
|
21
|
+
* @requires chalk
|
|
22
|
+
* @requires path
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const WorkflowService = require('../../services/WorkflowService');
|
|
26
|
+
const IssueService = require('../../services/IssueService');
|
|
27
|
+
const EpicService = require('../../services/EpicService');
|
|
28
|
+
const fs = require('fs-extra');
|
|
29
|
+
const ora = require('ora');
|
|
30
|
+
const chalk = require('chalk');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get service instances
|
|
35
|
+
* @returns {Object} Service instances
|
|
36
|
+
*/
|
|
37
|
+
async function getServices() {
|
|
38
|
+
const issueService = new IssueService();
|
|
39
|
+
const epicService = new EpicService();
|
|
40
|
+
const workflowService = new WorkflowService({
|
|
41
|
+
issueService,
|
|
42
|
+
epicService
|
|
43
|
+
});
|
|
44
|
+
return { workflowService, issueService, epicService };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* PM Next - Get next priority task
|
|
49
|
+
* @param {Object} argv - Command arguments
|
|
50
|
+
*/
|
|
51
|
+
async function pmNext(argv) {
|
|
52
|
+
const spinner = ora('Finding next priority task...').start();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const { workflowService } = await getServices();
|
|
56
|
+
const nextTask = await workflowService.getNextTask();
|
|
57
|
+
|
|
58
|
+
if (!nextTask) {
|
|
59
|
+
spinner.info(chalk.yellow('No available tasks found'));
|
|
60
|
+
|
|
61
|
+
console.log(chalk.yellow('\n⚠️ No tasks available to start\n'));
|
|
62
|
+
console.log(chalk.bold('Possible reasons:'));
|
|
63
|
+
console.log(' • All tasks are completed or in-progress');
|
|
64
|
+
console.log(' • Remaining tasks are blocked by dependencies');
|
|
65
|
+
console.log(' • No tasks have been created yet\n');
|
|
66
|
+
|
|
67
|
+
console.log(chalk.bold('💡 Suggestions:'));
|
|
68
|
+
console.log(` ${chalk.cyan('1.')} Check blocked tasks: ${chalk.yellow('autopm pm blocked')}`);
|
|
69
|
+
console.log(` ${chalk.cyan('2.')} Check active work: ${chalk.yellow('autopm pm in-progress')}`);
|
|
70
|
+
console.log(` ${chalk.cyan('3.')} View all tasks: ${chalk.yellow('autopm issue list')}\n`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
spinner.succeed(chalk.green('Found next task'));
|
|
75
|
+
|
|
76
|
+
// Display task with reasoning
|
|
77
|
+
console.log(chalk.cyan('\n📋 Next Task:\n'));
|
|
78
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
79
|
+
|
|
80
|
+
console.log(chalk.bold(`#${nextTask.id}: ${nextTask.title}`));
|
|
81
|
+
if (nextTask.epic) {
|
|
82
|
+
console.log(chalk.gray(`Epic: ${nextTask.epic}`));
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.gray(`Priority: ${nextTask.priority || 'P2'}`));
|
|
85
|
+
if (nextTask.effort) {
|
|
86
|
+
console.log(chalk.gray(`Estimated effort: ${nextTask.effort}`));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(chalk.yellow(`\n💡 Why this task?\n${nextTask.reasoning}`));
|
|
90
|
+
|
|
91
|
+
console.log('\n' + chalk.gray('─'.repeat(60)) + '\n');
|
|
92
|
+
|
|
93
|
+
// TDD Reminder
|
|
94
|
+
console.log(chalk.red('⚠️ TDD REMINDER - Before starting work:\n'));
|
|
95
|
+
console.log(chalk.dim(' 🚨 ALWAYS follow Test-Driven Development:'));
|
|
96
|
+
console.log(chalk.dim(' 1. RED: Write failing test first'));
|
|
97
|
+
console.log(chalk.dim(' 2. GREEN: Write minimal code to pass'));
|
|
98
|
+
console.log(chalk.dim(' 3. REFACTOR: Clean up while keeping tests green\n'));
|
|
99
|
+
|
|
100
|
+
console.log(chalk.green('✅ Ready to start?'));
|
|
101
|
+
console.log(` ${chalk.yellow(`autopm issue start ${nextTask.id}`)}\n`);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
spinner.fail(chalk.red('Failed to find next task'));
|
|
104
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* PM What-Next - AI-powered next step suggestions
|
|
110
|
+
* @param {Object} argv - Command arguments
|
|
111
|
+
*/
|
|
112
|
+
async function pmWhatNext(argv) {
|
|
113
|
+
const spinner = ora('Analyzing project state...').start();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const { workflowService } = await getServices();
|
|
117
|
+
const result = await workflowService.getWhatNext();
|
|
118
|
+
|
|
119
|
+
spinner.succeed(chalk.green('Analysis complete'));
|
|
120
|
+
|
|
121
|
+
console.log(chalk.cyan('\n🤔 What Should You Work On Next?\n'));
|
|
122
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
123
|
+
|
|
124
|
+
// Display project state
|
|
125
|
+
console.log(chalk.bold('📊 Current Project State:\n'));
|
|
126
|
+
console.log(` PRDs: ${result.projectState.prdCount}`);
|
|
127
|
+
console.log(` Epics: ${result.projectState.epicCount}`);
|
|
128
|
+
console.log(` Total Issues: ${result.projectState.issueCount}`);
|
|
129
|
+
console.log(` Open: ${result.projectState.openIssues}`);
|
|
130
|
+
console.log(` In Progress: ${result.projectState.inProgressIssues}`);
|
|
131
|
+
console.log(` Blocked: ${result.projectState.blockedIssues}\n`);
|
|
132
|
+
|
|
133
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
134
|
+
|
|
135
|
+
// Display suggestions
|
|
136
|
+
console.log(chalk.bold('💡 Suggested Next Steps:\n'));
|
|
137
|
+
|
|
138
|
+
result.suggestions.forEach((suggestion, index) => {
|
|
139
|
+
const marker = suggestion.recommended ? '⭐' : '○';
|
|
140
|
+
const priorityColor = suggestion.priority === 'high' ? chalk.red : chalk.yellow;
|
|
141
|
+
|
|
142
|
+
console.log(`${index + 1}. ${marker} ${chalk.bold(suggestion.title)}`);
|
|
143
|
+
console.log(` ${suggestion.description}`);
|
|
144
|
+
console.log(` ${priorityColor(`Priority: ${suggestion.priority.toUpperCase()}`)}`);
|
|
145
|
+
|
|
146
|
+
if (Array.isArray(suggestion.commands)) {
|
|
147
|
+
suggestion.commands.forEach(cmd => {
|
|
148
|
+
console.log(` ${chalk.yellow(cmd)}`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(` ${chalk.dim(`💭 ${suggestion.why}`)}\n`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (result.suggestions.length === 0) {
|
|
156
|
+
console.log(chalk.yellow(' No specific suggestions at this time\n'));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
spinner.fail(chalk.red('Failed to analyze project'));
|
|
162
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* PM Standup - Generate daily standup report
|
|
168
|
+
* @param {Object} argv - Command arguments
|
|
169
|
+
*/
|
|
170
|
+
async function pmStandup(argv) {
|
|
171
|
+
const spinner = ora('Generating standup report...').start();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const { workflowService } = await getServices();
|
|
175
|
+
const report = await workflowService.generateStandup();
|
|
176
|
+
|
|
177
|
+
spinner.succeed(chalk.green('Standup report generated'));
|
|
178
|
+
|
|
179
|
+
console.log(chalk.cyan(`\n📅 Daily Standup - ${report.date}\n`));
|
|
180
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
181
|
+
|
|
182
|
+
// Yesterday
|
|
183
|
+
console.log(chalk.bold('✅ Yesterday (Completed):\n'));
|
|
184
|
+
if (report.yesterday.length > 0) {
|
|
185
|
+
report.yesterday.forEach(task => {
|
|
186
|
+
console.log(` #${task.id} - ${task.title}`);
|
|
187
|
+
if (task.epic) {
|
|
188
|
+
console.log(` ${chalk.gray(`Epic: ${task.epic}`)}`);
|
|
189
|
+
}
|
|
190
|
+
console.log('');
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
console.log(chalk.gray(' No tasks completed yesterday\n'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
197
|
+
|
|
198
|
+
// Today
|
|
199
|
+
console.log(chalk.bold('🚀 Today (In Progress):\n'));
|
|
200
|
+
if (report.today.length > 0) {
|
|
201
|
+
report.today.forEach(task => {
|
|
202
|
+
console.log(` #${task.id} - ${task.title || 'Unnamed task'}`);
|
|
203
|
+
if (task.stale) {
|
|
204
|
+
console.log(` ${chalk.red('⚠️ STALE (>3 days in progress)')}`);
|
|
205
|
+
}
|
|
206
|
+
console.log('');
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
console.log(chalk.gray(' No tasks currently in progress\n'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
213
|
+
|
|
214
|
+
// Blockers
|
|
215
|
+
console.log(chalk.bold('🚫 Blockers:\n'));
|
|
216
|
+
if (report.blockers.length > 0) {
|
|
217
|
+
report.blockers.forEach(task => {
|
|
218
|
+
console.log(` #${task.id} - ${task.title || 'Unnamed task'}`);
|
|
219
|
+
console.log(` ${chalk.red(`Blocked by: ${task.reason}`)}`);
|
|
220
|
+
if (task.daysBlocked) {
|
|
221
|
+
console.log(` ${chalk.gray(`Blocked for: ${task.daysBlocked} days`)}`);
|
|
222
|
+
}
|
|
223
|
+
console.log('');
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
console.log(chalk.green(' No blockers! 🎉\n'));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
230
|
+
|
|
231
|
+
// Metrics
|
|
232
|
+
console.log(chalk.bold('📊 Metrics:\n'));
|
|
233
|
+
console.log(` Velocity: ${report.velocity} tasks/day (7-day avg)`);
|
|
234
|
+
console.log(` Sprint Progress: ${report.sprintProgress.completed}/${report.sprintProgress.total} (${report.sprintProgress.percentage}%)\n`);
|
|
235
|
+
|
|
236
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
237
|
+
} catch (error) {
|
|
238
|
+
spinner.fail(chalk.red('Failed to generate standup'));
|
|
239
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* PM Status - Project status overview
|
|
245
|
+
* @param {Object} argv - Command arguments
|
|
246
|
+
*/
|
|
247
|
+
async function pmStatus(argv) {
|
|
248
|
+
const spinner = ora('Analyzing project status...').start();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const { workflowService } = await getServices();
|
|
252
|
+
const status = await workflowService.getProjectStatus();
|
|
253
|
+
|
|
254
|
+
spinner.succeed(chalk.green('Status analysis complete'));
|
|
255
|
+
|
|
256
|
+
console.log(chalk.cyan('\n📊 Project Status Overview\n'));
|
|
257
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
258
|
+
|
|
259
|
+
// Epics
|
|
260
|
+
console.log(chalk.bold('📚 Epics:\n'));
|
|
261
|
+
console.log(` Backlog: ${status.epics.backlog}`);
|
|
262
|
+
console.log(` Planning: ${status.epics.planning}`);
|
|
263
|
+
console.log(` In Progress: ${status.epics.inProgress}`);
|
|
264
|
+
console.log(` Completed: ${status.epics.completed}`);
|
|
265
|
+
console.log(` ${chalk.bold('Total:')} ${status.epics.total}\n`);
|
|
266
|
+
|
|
267
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
268
|
+
|
|
269
|
+
// Issues
|
|
270
|
+
console.log(chalk.bold('📋 Issues:\n'));
|
|
271
|
+
console.log(` Open: ${status.issues.open}`);
|
|
272
|
+
console.log(` In Progress: ${status.issues.inProgress}`);
|
|
273
|
+
console.log(` Blocked: ${chalk.red(status.issues.blocked)}`);
|
|
274
|
+
console.log(` Closed: ${chalk.green(status.issues.closed)}`);
|
|
275
|
+
console.log(` ${chalk.bold('Total:')} ${status.issues.total}\n`);
|
|
276
|
+
|
|
277
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
278
|
+
|
|
279
|
+
// Progress
|
|
280
|
+
console.log(chalk.bold('📈 Progress:\n'));
|
|
281
|
+
console.log(` Overall: ${status.progress.overall}% complete`);
|
|
282
|
+
console.log(` Velocity: ${status.progress.velocity} tasks/day\n`);
|
|
283
|
+
|
|
284
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
285
|
+
|
|
286
|
+
// Health
|
|
287
|
+
const healthColor = status.health === 'ON_TRACK' ? chalk.green : chalk.red;
|
|
288
|
+
console.log(chalk.bold('🎯 Health: ') + healthColor(status.health) + '\n');
|
|
289
|
+
|
|
290
|
+
if (status.recommendations.length > 0) {
|
|
291
|
+
console.log(chalk.bold('💡 Recommendations:\n'));
|
|
292
|
+
status.recommendations.forEach((rec, index) => {
|
|
293
|
+
console.log(` ${index + 1}. ${rec}`);
|
|
294
|
+
});
|
|
295
|
+
console.log('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
299
|
+
} catch (error) {
|
|
300
|
+
spinner.fail(chalk.red('Failed to analyze status'));
|
|
301
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* PM In-Progress - Show all active tasks
|
|
307
|
+
* @param {Object} argv - Command arguments
|
|
308
|
+
*/
|
|
309
|
+
async function pmInProgress(argv) {
|
|
310
|
+
const spinner = ora('Finding active tasks...').start();
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const { workflowService } = await getServices();
|
|
314
|
+
const tasks = await workflowService.getInProgressTasks();
|
|
315
|
+
|
|
316
|
+
spinner.succeed(chalk.green('Active tasks found'));
|
|
317
|
+
|
|
318
|
+
console.log(chalk.cyan('\n🚀 In Progress Tasks\n'));
|
|
319
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
320
|
+
|
|
321
|
+
if (tasks.length === 0) {
|
|
322
|
+
console.log(chalk.yellow('No tasks currently in progress\n'));
|
|
323
|
+
console.log(chalk.bold('💡 Ready to start work?'));
|
|
324
|
+
console.log(` ${chalk.yellow('autopm pm next')}\n`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Group by epic if available
|
|
329
|
+
const byEpic = {};
|
|
330
|
+
tasks.forEach(task => {
|
|
331
|
+
const epic = task.epic || 'No Epic';
|
|
332
|
+
if (!byEpic[epic]) {
|
|
333
|
+
byEpic[epic] = [];
|
|
334
|
+
}
|
|
335
|
+
byEpic[epic].push(task);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
Object.keys(byEpic).forEach(epicName => {
|
|
339
|
+
console.log(chalk.bold(`Epic: ${epicName}\n`));
|
|
340
|
+
|
|
341
|
+
byEpic[epicName].forEach(task => {
|
|
342
|
+
console.log(` #${task.id} - ${task.title || 'Unnamed task'}`);
|
|
343
|
+
if (task.started) {
|
|
344
|
+
console.log(` ${chalk.gray(`Started: ${new Date(task.started).toLocaleDateString()}`)}`);
|
|
345
|
+
}
|
|
346
|
+
if (task.assignee) {
|
|
347
|
+
console.log(` ${chalk.gray(`Assignee: ${task.assignee}`)}`);
|
|
348
|
+
}
|
|
349
|
+
if (task.stale) {
|
|
350
|
+
console.log(` ${chalk.red('⚠️ STALE (>3 days without update)')}`);
|
|
351
|
+
console.log(` ${chalk.yellow('💡 Consider checking in or splitting task')}`);
|
|
352
|
+
}
|
|
353
|
+
console.log('');
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Summary
|
|
358
|
+
const staleCount = tasks.filter(t => t.stale).length;
|
|
359
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
360
|
+
console.log(chalk.bold('📊 Summary:\n'));
|
|
361
|
+
console.log(` Total active: ${tasks.length} tasks`);
|
|
362
|
+
if (staleCount > 0) {
|
|
363
|
+
console.log(` ${chalk.red(`⚠️ Stale: ${staleCount} tasks (>3 days)`)}`);
|
|
364
|
+
}
|
|
365
|
+
console.log('');
|
|
366
|
+
|
|
367
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
368
|
+
} catch (error) {
|
|
369
|
+
spinner.fail(chalk.red('Failed to find active tasks'));
|
|
370
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* PM Blocked - Show all blocked tasks
|
|
376
|
+
* @param {Object} argv - Command arguments
|
|
377
|
+
*/
|
|
378
|
+
async function pmBlocked(argv) {
|
|
379
|
+
const spinner = ora('Finding blocked tasks...').start();
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const { workflowService } = await getServices();
|
|
383
|
+
const tasks = await workflowService.getBlockedTasks();
|
|
384
|
+
|
|
385
|
+
spinner.succeed(chalk.green('Blocked tasks analyzed'));
|
|
386
|
+
|
|
387
|
+
console.log(chalk.cyan('\n🚫 Blocked Tasks\n'));
|
|
388
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
389
|
+
|
|
390
|
+
if (tasks.length === 0) {
|
|
391
|
+
console.log(chalk.green('✅ No blocked tasks! All tasks are unblocked.\n'));
|
|
392
|
+
console.log(chalk.bold('💡 Ready to work?'));
|
|
393
|
+
console.log(` ${chalk.yellow('autopm pm next')}\n`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
tasks.forEach((task, index) => {
|
|
398
|
+
console.log(`${index + 1}. ${chalk.bold(`#${task.id} - ${task.title || 'Unnamed task'}`)}`);
|
|
399
|
+
console.log(` ${chalk.red(`Blocked by: ${task.reason}`)}`);
|
|
400
|
+
if (task.daysBlocked !== undefined) {
|
|
401
|
+
const daysLabel = task.daysBlocked === 1 ? 'day' : 'days';
|
|
402
|
+
const daysColor = task.daysBlocked > 3 ? chalk.red : chalk.yellow;
|
|
403
|
+
console.log(` ${daysColor(`Blocked since: ${task.daysBlocked} ${daysLabel} ago`)}`);
|
|
404
|
+
}
|
|
405
|
+
if (task.suggestedAction) {
|
|
406
|
+
console.log(` ${chalk.yellow(`💡 Action: ${task.suggestedAction}`)}`);
|
|
407
|
+
}
|
|
408
|
+
console.log('');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Summary
|
|
412
|
+
const criticalCount = tasks.filter(t => t.daysBlocked > 3).length;
|
|
413
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
414
|
+
console.log(chalk.bold('📊 Summary:\n'));
|
|
415
|
+
console.log(` Total blocked: ${tasks.length} tasks`);
|
|
416
|
+
if (criticalCount > 0) {
|
|
417
|
+
console.log(` ${chalk.red(`🔴 Critical: ${criticalCount} tasks (>3 days)`)}`);
|
|
418
|
+
}
|
|
419
|
+
console.log('');
|
|
420
|
+
|
|
421
|
+
console.log(chalk.bold('💡 Recommendations:\n'));
|
|
422
|
+
console.log(' 1. Unblock critical tasks first (>3 days)');
|
|
423
|
+
console.log(' 2. Review and resolve dependencies');
|
|
424
|
+
console.log(' 3. Update stakeholders on delays\n');
|
|
425
|
+
|
|
426
|
+
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
427
|
+
} catch (error) {
|
|
428
|
+
spinner.fail(chalk.red('Failed to find blocked tasks'));
|
|
429
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Command builder - registers all subcommands
|
|
435
|
+
* @param {Object} yargs - Yargs instance
|
|
436
|
+
* @returns {Object} Configured yargs instance
|
|
437
|
+
*/
|
|
438
|
+
function builder(yargs) {
|
|
439
|
+
return yargs
|
|
440
|
+
.command(
|
|
441
|
+
'next',
|
|
442
|
+
'Get next priority task',
|
|
443
|
+
(yargs) => {
|
|
444
|
+
return yargs
|
|
445
|
+
.example('autopm pm next', 'Show next priority task to work on');
|
|
446
|
+
},
|
|
447
|
+
pmNext
|
|
448
|
+
)
|
|
449
|
+
.command(
|
|
450
|
+
'what-next',
|
|
451
|
+
'AI-powered next step suggestions',
|
|
452
|
+
(yargs) => {
|
|
453
|
+
return yargs
|
|
454
|
+
.example('autopm pm what-next', 'Get intelligent suggestions for next steps');
|
|
455
|
+
},
|
|
456
|
+
pmWhatNext
|
|
457
|
+
)
|
|
458
|
+
.command(
|
|
459
|
+
'standup',
|
|
460
|
+
'Generate daily standup report',
|
|
461
|
+
(yargs) => {
|
|
462
|
+
return yargs
|
|
463
|
+
.example('autopm pm standup', 'Generate daily standup summary');
|
|
464
|
+
},
|
|
465
|
+
pmStandup
|
|
466
|
+
)
|
|
467
|
+
.command(
|
|
468
|
+
'status',
|
|
469
|
+
'Project status overview',
|
|
470
|
+
(yargs) => {
|
|
471
|
+
return yargs
|
|
472
|
+
.example('autopm pm status', 'Show overall project health and metrics');
|
|
473
|
+
},
|
|
474
|
+
pmStatus
|
|
475
|
+
)
|
|
476
|
+
.command(
|
|
477
|
+
'in-progress',
|
|
478
|
+
'Show all active tasks',
|
|
479
|
+
(yargs) => {
|
|
480
|
+
return yargs
|
|
481
|
+
.example('autopm pm in-progress', 'List all tasks currently being worked on');
|
|
482
|
+
},
|
|
483
|
+
pmInProgress
|
|
484
|
+
)
|
|
485
|
+
.command(
|
|
486
|
+
'blocked',
|
|
487
|
+
'Show all blocked tasks',
|
|
488
|
+
(yargs) => {
|
|
489
|
+
return yargs
|
|
490
|
+
.example('autopm pm blocked', 'List all blocked tasks with reasons');
|
|
491
|
+
},
|
|
492
|
+
pmBlocked
|
|
493
|
+
)
|
|
494
|
+
.demandCommand(1, 'You must specify a pm command')
|
|
495
|
+
.strictCommands()
|
|
496
|
+
.help();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Command export
|
|
501
|
+
*/
|
|
502
|
+
module.exports = {
|
|
503
|
+
command: 'pm',
|
|
504
|
+
describe: 'Project management and workflow commands',
|
|
505
|
+
builder,
|
|
506
|
+
handler: (argv) => {
|
|
507
|
+
if (!argv._.includes('pm') || argv._.length === 1) {
|
|
508
|
+
console.log(chalk.yellow('\nPlease specify a pm command\n'));
|
|
509
|
+
console.log('Usage: autopm pm <command>\n');
|
|
510
|
+
console.log('Available commands:');
|
|
511
|
+
console.log(' next Get next priority task');
|
|
512
|
+
console.log(' what-next AI-powered next step suggestions');
|
|
513
|
+
console.log(' standup Generate daily standup report');
|
|
514
|
+
console.log(' status Project status overview');
|
|
515
|
+
console.log(' in-progress Show all active tasks');
|
|
516
|
+
console.log(' blocked Show all blocked tasks');
|
|
517
|
+
console.log('\nUse: autopm pm <command> --help for more info\n');
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
handlers: {
|
|
521
|
+
next: pmNext,
|
|
522
|
+
whatNext: pmWhatNext,
|
|
523
|
+
standup: pmStandup,
|
|
524
|
+
status: pmStatus,
|
|
525
|
+
inProgress: pmInProgress,
|
|
526
|
+
blocked: pmBlocked
|
|
527
|
+
}
|
|
528
|
+
};
|
|
@@ -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;
|