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 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-autopm",
3
- "version": "2.5.0",
3
+ "version": "2.6.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": {