@hyperdrive.bot/bmad-workflow 1.0.17 → 1.0.19

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.
Files changed (110) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +34 -75
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +89 -0
  36. package/dist/commands/workflow.js +487 -14
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/index.d.ts +1 -0
  45. package/dist/models/index.js +1 -0
  46. package/dist/models/workflow-callbacks.d.ts +251 -0
  47. package/dist/models/workflow-callbacks.js +10 -0
  48. package/dist/models/workflow-config.d.ts +77 -0
  49. package/dist/models/workflow-result.d.ts +7 -0
  50. package/dist/services/WorkflowReporter.d.ts +165 -0
  51. package/dist/services/WorkflowReporter.js +691 -0
  52. package/dist/services/agents/claude-agent-runner.js +25 -4
  53. package/dist/services/file-system/path-resolver.d.ts +10 -0
  54. package/dist/services/file-system/path-resolver.js +12 -0
  55. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  56. package/dist/services/mcp/mcp-config-manager.js +146 -0
  57. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  58. package/dist/services/mcp/mcp-context-injector.js +168 -0
  59. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  60. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  61. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  62. package/dist/services/mcp/mcp-health-checker.js +162 -0
  63. package/dist/services/mcp/types/health-types.d.ts +31 -0
  64. package/dist/services/mcp/types/health-types.js +7 -0
  65. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  66. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  67. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  68. package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
  69. package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
  70. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  71. package/dist/services/review/ai-review-scanner.js +142 -0
  72. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  73. package/dist/services/review/coderabbit-scanner.js +31 -0
  74. package/dist/services/review/index.d.ts +20 -0
  75. package/dist/services/review/index.js +15 -0
  76. package/dist/services/review/lint-scanner.d.ts +46 -0
  77. package/dist/services/review/lint-scanner.js +172 -0
  78. package/dist/services/review/review-config.d.ts +62 -0
  79. package/dist/services/review/review-config.js +91 -0
  80. package/dist/services/review/review-phase-executor.d.ts +69 -0
  81. package/dist/services/review/review-phase-executor.js +152 -0
  82. package/dist/services/review/review-queue.d.ts +98 -0
  83. package/dist/services/review/review-queue.js +174 -0
  84. package/dist/services/review/review-reporter.d.ts +94 -0
  85. package/dist/services/review/review-reporter.js +386 -0
  86. package/dist/services/review/scanner-factory.d.ts +42 -0
  87. package/dist/services/review/scanner-factory.js +60 -0
  88. package/dist/services/review/self-heal-loop.d.ts +58 -0
  89. package/dist/services/review/self-heal-loop.js +132 -0
  90. package/dist/services/review/severity-classifier.d.ts +17 -0
  91. package/dist/services/review/severity-classifier.js +314 -0
  92. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  93. package/dist/services/review/tech-debt-tracker.js +245 -0
  94. package/dist/services/review/types.d.ts +93 -0
  95. package/dist/services/review/types.js +23 -0
  96. package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
  97. package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
  98. package/dist/services/validation/config-validator.d.ts +84 -0
  99. package/dist/services/validation/config-validator.js +78 -0
  100. package/dist/utils/colors.d.ts +10 -10
  101. package/dist/utils/colors.js +15 -15
  102. package/dist/utils/credential-utils.d.ts +14 -0
  103. package/dist/utils/credential-utils.js +19 -0
  104. package/dist/utils/duration.d.ts +41 -0
  105. package/dist/utils/duration.js +89 -0
  106. package/dist/utils/listr2-helpers.d.ts +216 -0
  107. package/dist/utils/listr2-helpers.js +334 -0
  108. package/dist/utils/shared-flags.d.ts +1 -0
  109. package/dist/utils/shared-flags.js +11 -2
  110. package/package.json +6 -3
@@ -1,5 +1,9 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
+ import { basename } from 'node:path';
3
+ import { parseDuration } from '../utils/duration.js';
2
4
  import { createAgentRunner, isProviderSupported } from '../services/agents/agent-runner-factory.js';
5
+ import { getRegisteredScannerNames } from '../services/review/scanner-factory.js';
6
+ import { Severity } from '../services/review/types.js';
3
7
  import { FileManager } from '../services/file-system/file-manager.js';
4
8
  import { PathResolver } from '../services/file-system/path-resolver.js';
5
9
  import { BatchProcessor } from '../services/orchestration/batch-processor.js';
@@ -8,6 +12,8 @@ import { StoryTypeDetector } from '../services/orchestration/story-type-detector
8
12
  import { WorkflowOrchestrator } from '../services/orchestration/workflow-orchestrator.js';
9
13
  import { EpicParser } from '../services/parsers/epic-parser.js';
10
14
  import { PrdParser } from '../services/parsers/prd-parser.js';
15
+ import { WorkflowSessionScaffolder } from '../services/scaffolding/workflow-session-scaffolder.js';
16
+ import { WorkflowReporter } from '../services/WorkflowReporter.js';
11
17
  import * as colors from '../utils/colors.js';
12
18
  import { formatBox, formatTable } from '../utils/formatters.js';
13
19
  import { createLogger } from '../utils/logger.js';
@@ -77,13 +83,47 @@ export default class Workflow extends Command {
77
83
  }),
78
84
  prefix: Flags.string({
79
85
  default: '',
80
- description: 'Filename prefix for generated files',
86
+ description: 'Filename prefix for generated files and session directory name',
87
+ }),
88
+ 'session-prefix': Flags.string({
89
+ description: 'Session directory prefix (default: derived from input filename, e.g., PRD-feature.md → feature)',
81
90
  }),
82
91
  provider: Flags.string({
83
92
  default: 'claude',
84
93
  description: 'AI provider to use (claude, gemini, or opencode)',
85
94
  options: ['claude', 'gemini', 'opencode'],
86
95
  }),
96
+ mcp: Flags.boolean({
97
+ default: false,
98
+ description: 'Enable MCP tool injection for pipeline phases via Docker gateway',
99
+ helpGroup: 'MCP',
100
+ }),
101
+ 'mcp-phases': Flags.string({
102
+ description: 'Comma-separated list of phases to inject MCP tools into (valid: epic,story,dev,review,qa)',
103
+ helpGroup: 'MCP',
104
+ }),
105
+ 'mcp-preset': Flags.string({
106
+ description: 'MCP preset name to use for tool configuration (overrides config file)',
107
+ helpGroup: 'MCP',
108
+ }),
109
+ review: Flags.boolean({
110
+ default: false,
111
+ description: 'Enable automated code review phase between dev and QA',
112
+ helpGroup: 'Review Workflow',
113
+ }),
114
+ 'review-block-on': Flags.string({
115
+ description: 'Minimum severity to block story from QA (critical|high|medium|low)',
116
+ helpGroup: 'Review Workflow',
117
+ }),
118
+ 'review-max-fix': Flags.integer({
119
+ description: 'Maximum self-heal fix iterations (default: from config or 3)',
120
+ helpGroup: 'Review Workflow',
121
+ }),
122
+ 'review-scanners': Flags.string({
123
+ description: 'Scanner names to run (e.g., lint, ai, coderabbit). Repeatable.',
124
+ helpGroup: 'Review Workflow',
125
+ multiple: true,
126
+ }),
87
127
  qa: Flags.boolean({
88
128
  default: false,
89
129
  description: 'Run QA workflow after development completes',
@@ -118,9 +158,17 @@ export default class Workflow extends Command {
118
158
  default: 60,
119
159
  description: 'Seconds between story development',
120
160
  }),
121
- timeout: Flags.integer({
161
+ timeout: Flags.custom({
162
+ parse: async (input) => parseDuration(input),
163
+ })({
122
164
  default: 2_700_000,
123
- description: 'Agent execution timeout in milliseconds (default: 2700000 = 45 minutes)',
165
+ description: 'Agent execution timeout accepts durations like 30s, 5m, 1h, 90m, or raw milliseconds (default: 45m)',
166
+ }),
167
+ 'review-timeout': Flags.custom({
168
+ parse: async (input) => parseDuration(input),
169
+ })({
170
+ description: 'AI review scanner timeout — overrides --timeout for review phase only (default: 5m)',
171
+ helpGroup: 'Review Workflow',
124
172
  }),
125
173
  'max-retries': Flags.integer({
126
174
  default: 0,
@@ -133,12 +181,15 @@ export default class Workflow extends Command {
133
181
  verbose: Flags.boolean({
134
182
  char: 'v',
135
183
  default: false,
136
- description: 'Detailed output mode',
184
+ description: 'Show detailed output with spawn output streamed inline. In verbose mode, each spawn\'s output is displayed in real-time with visual delimiters. In non-TTY environments (e.g., CI/CD pipelines), output automatically falls back to plain-text format without ANSI escape codes.',
137
185
  }),
138
186
  };
139
187
  cancelled = false;
140
188
  logger;
141
189
  orchestrator;
190
+ reporter;
191
+ scaffolder;
192
+ sessionDir = null;
142
193
  /**
143
194
  * Main command execution
144
195
  *
@@ -160,13 +211,44 @@ export default class Workflow extends Command {
160
211
  this.error(`Unsupported provider: ${flags.provider}. Use 'claude', 'gemini', or 'opencode'.`, { exit: 1 });
161
212
  }
162
213
  // Initialize services with parallel concurrency and provider
163
- await this.initializeServices(flags.parallel, flags.provider);
214
+ await this.initializeServices(flags.parallel, flags.provider, flags.verbose);
164
215
  // Register signal handlers
165
216
  this.registerSignalHandlers();
217
+ // Initialize session scaffolder and create session structure
218
+ await this.initializeSession(args.input, flags['session-prefix']);
166
219
  // Show dry-run banner if applicable
167
220
  if (flags['dry-run']) {
168
221
  this.displayDryRunBanner();
169
222
  }
223
+ // Validate review flags when --review is enabled
224
+ if (flags.review) {
225
+ // Validate --review-scanners against registered scanner names
226
+ if (flags['review-scanners'] && flags['review-scanners'].length > 0) {
227
+ const validScanners = getRegisteredScannerNames();
228
+ const invalidScanners = flags['review-scanners'].filter((s) => !validScanners.includes(s));
229
+ if (invalidScanners.length > 0) {
230
+ this.error(`Unknown --review-scanners value(s): ${invalidScanners.join(', ')}. Valid scanners are: ${validScanners.join(', ')}`, { exit: 1 });
231
+ }
232
+ }
233
+ // Validate --review-max-fix is non-negative
234
+ if (flags['review-max-fix'] !== undefined && flags['review-max-fix'] < 0) {
235
+ this.error(`--review-max-fix must be a non-negative integer, got: ${flags['review-max-fix']}`, { exit: 1 });
236
+ }
237
+ // Validate --review-block-on against severity enum
238
+ if (flags['review-block-on'] !== undefined) {
239
+ const validSeverities = ['critical', 'high', 'medium', 'low'];
240
+ if (!validSeverities.includes(flags['review-block-on'])) {
241
+ this.error(`Invalid --review-block-on value: ${flags['review-block-on']}. Valid values are: ${validSeverities.join(', ')}`, { exit: 1 });
242
+ }
243
+ }
244
+ }
245
+ // Map --review-block-on string to Severity enum
246
+ const severityMap = {
247
+ critical: Severity.CRITICAL,
248
+ high: Severity.HIGH,
249
+ low: Severity.LOW,
250
+ medium: Severity.MEDIUM,
251
+ };
170
252
  // Build workflow configuration
171
253
  const config = {
172
254
  autoFix: flags['auto-fix'],
@@ -175,6 +257,9 @@ export default class Workflow extends Command {
175
257
  epicInterval: flags['epic-interval'],
176
258
  input: args.input,
177
259
  maxRetries: flags['max-retries'],
260
+ mcp: flags.mcp || undefined,
261
+ mcpPhases: flags['mcp-phases'] ? flags['mcp-phases'].split(',').map((s) => s.trim()) : undefined,
262
+ mcpPreset: flags['mcp-preset'],
178
263
  model: flags.model,
179
264
  parallel: flags.parallel,
180
265
  pipeline: flags.pipeline,
@@ -186,6 +271,11 @@ export default class Workflow extends Command {
186
271
  qaRetries: flags['qa-retries'],
187
272
  references: flags.reference || [],
188
273
  retryBackoffMs: flags['retry-backoff'],
274
+ review: flags.review,
275
+ reviewBlockOn: flags['review-block-on'] ? severityMap[flags['review-block-on']] : undefined,
276
+ reviewMaxFix: flags['review-max-fix'],
277
+ reviewScanners: flags['review-scanners'],
278
+ reviewTimeout: flags['review-timeout'],
189
279
  skipDev: flags['skip-dev'],
190
280
  skipEpics: flags['skip-epics'],
191
281
  skipStories: flags['skip-stories'],
@@ -193,6 +283,35 @@ export default class Workflow extends Command {
193
283
  timeout: flags.timeout,
194
284
  verbose: flags.verbose,
195
285
  };
286
+ // Validate --mcp-phases when --mcp is enabled
287
+ if (config.mcp && config.mcpPhases) {
288
+ const validPhases = ['epic', 'story', 'dev', 'review', 'qa'];
289
+ const invalidPhases = config.mcpPhases.filter((p) => !validPhases.includes(p));
290
+ if (invalidPhases.length > 0) {
291
+ this.error(`Invalid --mcp-phases value(s): ${invalidPhases.join(', ')}. Valid phases are: ${validPhases.join(', ')}`, { exit: 1 });
292
+ }
293
+ }
294
+ // MCP gateway health pre-check (AC: #5, #6)
295
+ if (config.mcp) {
296
+ try {
297
+ const gatewayUrl = 'http://localhost:8080';
298
+ const healthUrl = `${gatewayUrl}/health`;
299
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
300
+ if (response.ok) {
301
+ if (flags.verbose) {
302
+ this.log(colors.info('MCP gateway is healthy'));
303
+ }
304
+ }
305
+ else {
306
+ this.log(colors.warning(`MCP gateway returned status ${response.status} — continuing without MCP`));
307
+ config.mcp = false;
308
+ }
309
+ }
310
+ catch {
311
+ this.log(colors.warning('MCP gateway is not reachable — continuing without MCP'));
312
+ config.mcp = false;
313
+ }
314
+ }
196
315
  // Log configuration if verbose
197
316
  if (flags.verbose) {
198
317
  this.logger.info({ config }, 'Workflow configuration');
@@ -201,21 +320,37 @@ export default class Workflow extends Command {
201
320
  // Execute workflow
202
321
  this.log(colors.info('\nStarting workflow orchestration...\n'));
203
322
  const result = await this.orchestrator.execute(config);
323
+ // Wait for reporter listr2 tasks to complete and clean up
324
+ await this.reporter?.waitForCompletion();
325
+ // Write session report on completion (includes cancellation case)
326
+ await this.finalizeSession(result, config);
327
+ // Build dashboard summary from workflow result
328
+ const dashboardSummary = this.buildDashboardSummary(result);
329
+ // Display final dashboard using reporter (AC: #4)
330
+ this.reporter?.displayFinalDashboard(dashboardSummary);
331
+ this.reporter?.dispose();
332
+ // Display failure details if any
333
+ if (result.totalFailures > 0) {
334
+ this.displayFailureDetails(result);
335
+ }
336
+ // Dry run reminder
337
+ if (config.dryRun) {
338
+ this.log('\n' + colors.warning('No files were created (dry-run mode)'));
339
+ }
204
340
  // Check for cancellation
205
341
  if (this.cancelled) {
206
342
  this.log('\n' + colors.warning('Workflow cancelled by user'));
207
- this.displayFinalSummary(result, config);
208
343
  process.exit(130); // Standard interrupted exit code
209
344
  }
210
- // Display final summary
211
- this.displayFinalSummary(result, config);
212
345
  // Exit with appropriate code
213
346
  if (!result.overallSuccess) {
214
347
  this.error('Workflow completed with failures', { exit: 1 });
215
348
  }
216
- this.log('\n' + colors.success('Workflow completed successfully!'));
349
+ this.log('\n' + colors.success('Workflow completed successfully!'));
217
350
  }
218
351
  catch (error) {
352
+ // Clean up reporter on error
353
+ this.reporter?.dispose();
219
354
  this.logger?.error({ error: error.message }, 'Workflow command failed');
220
355
  // Display user-friendly error
221
356
  this.log('\n' + colors.error('✗ Workflow failed:'));
@@ -223,6 +358,251 @@ export default class Workflow extends Command {
223
358
  this.error('Workflow execution failed', { exit: 1 });
224
359
  }
225
360
  }
361
+ /**
362
+ * Collect all errors from workflow result into a flat list
363
+ *
364
+ * @param result - Workflow execution result
365
+ * @returns Array of error entries with phase context
366
+ * @private
367
+ */
368
+ collectErrors(result) {
369
+ const errors = [];
370
+ const now = new Date().toISOString();
371
+ if (result.epicPhase?.failures) {
372
+ for (const failure of result.epicPhase.failures) {
373
+ errors.push({
374
+ error: failure.error,
375
+ identifier: failure.identifier,
376
+ phase: 'epic',
377
+ timestamp: now,
378
+ });
379
+ }
380
+ }
381
+ if (result.storyPhase?.failures) {
382
+ for (const failure of result.storyPhase.failures) {
383
+ errors.push({
384
+ error: failure.error,
385
+ identifier: failure.identifier,
386
+ phase: 'story',
387
+ timestamp: now,
388
+ });
389
+ }
390
+ }
391
+ if (result.devPhase?.failures) {
392
+ for (const failure of result.devPhase.failures) {
393
+ errors.push({
394
+ error: failure.error,
395
+ identifier: failure.identifier,
396
+ phase: 'dev',
397
+ timestamp: now,
398
+ });
399
+ }
400
+ }
401
+ if (result.qaPhase?.failures) {
402
+ for (const failure of result.qaPhase.failures) {
403
+ errors.push({
404
+ error: failure.error,
405
+ identifier: failure.identifier,
406
+ phase: 'qa',
407
+ timestamp: now,
408
+ });
409
+ }
410
+ }
411
+ return errors;
412
+ }
413
+ /**
414
+ * Build dashboard summary from workflow result for final display
415
+ *
416
+ * @param result - Workflow execution result
417
+ * @returns DashboardSummary for the reporter's final dashboard
418
+ * @private
419
+ */
420
+ buildDashboardSummary(result) {
421
+ const phases = [];
422
+ // Add phases in order: epic, story, dev, qa
423
+ if (result.epicPhase && !result.epicPhase.skipped) {
424
+ phases.push({
425
+ duration: result.epicPhase.duration,
426
+ failed: result.epicPhase.failures.length,
427
+ name: 'epic',
428
+ passed: result.epicPhase.success,
429
+ });
430
+ }
431
+ if (result.storyPhase && !result.storyPhase.skipped) {
432
+ phases.push({
433
+ duration: result.storyPhase.duration,
434
+ failed: result.storyPhase.failures.length,
435
+ name: 'story',
436
+ passed: result.storyPhase.success,
437
+ });
438
+ }
439
+ if (result.devPhase && !result.devPhase.skipped) {
440
+ phases.push({
441
+ duration: result.devPhase.duration,
442
+ failed: result.devPhase.failures.length,
443
+ name: 'dev',
444
+ passed: result.devPhase.success,
445
+ });
446
+ }
447
+ if (result.qaPhase && !result.qaPhase.skipped) {
448
+ phases.push({
449
+ duration: result.qaPhase.duration,
450
+ failed: result.qaPhase.failures.length,
451
+ name: 'qa',
452
+ passed: result.qaPhase.success,
453
+ });
454
+ }
455
+ // Build session report path from session directory
456
+ const sessionReportPath = this.sessionDir ? `${this.sessionDir}/SESSION_REPORT.md` : undefined;
457
+ return {
458
+ failedCount: result.totalFailures,
459
+ passedCount: result.totalFilesProcessed - result.totalFailures,
460
+ phases,
461
+ sessionReportPath,
462
+ totalDuration: result.totalDuration,
463
+ totalSpawns: result.totalFilesProcessed,
464
+ };
465
+ }
466
+ /**
467
+ * Create callbacks object wired to scaffolder for incremental artifact writing
468
+ *
469
+ * All callbacks are wrapped in try-catch to prevent scaffolder errors from
470
+ * interrupting workflow execution (AC: #9).
471
+ * @returns WorkflowCallbacks object with scaffolder integration
472
+ * @private
473
+ */
474
+ createScaffolderCallbacks() {
475
+ return {
476
+ onSpawnComplete: (context) => {
477
+ // Write spawn output immediately for crash resilience (AC: #8)
478
+ try {
479
+ // Map phaseName to spawn type
480
+ const typeMap = {
481
+ dev: 'dev',
482
+ epic: 'epic',
483
+ story: 'story',
484
+ };
485
+ const spawnType = typeMap[context.phaseName] || 'dev';
486
+ // Fire-and-forget write to not block workflow
487
+ this.scaffolder
488
+ .writeSpawnOutput({
489
+ completedAt: context.endTime ? new Date(context.endTime).toISOString() : undefined,
490
+ content: context.output || '',
491
+ duration: context.duration,
492
+ errors: context.error,
493
+ spawnId: context.spawnId,
494
+ success: context.success,
495
+ type: spawnType,
496
+ })
497
+ .catch((writeError) => {
498
+ this.logger.warn({ error: writeError.message, spawnId: context.spawnId }, 'Async spawn output write failed');
499
+ });
500
+ }
501
+ catch (error) {
502
+ // Log but don't propagate - workflow must continue (AC: #9)
503
+ this.logger.warn({ error: error.message, spawnId: context.spawnId }, 'Failed to write spawn output');
504
+ }
505
+ },
506
+ };
507
+ }
508
+ /**
509
+ * Merge multiple callback objects into a single callbacks object
510
+ *
511
+ * When the same callback is defined in multiple sources, all handlers are called
512
+ * in sequence (fire-and-forget). This enables dual-channel output where scaffolder
513
+ * and reporter receive events independently.
514
+ *
515
+ * @param sources - Array of WorkflowCallbacks objects to merge
516
+ * @returns Merged WorkflowCallbacks object
517
+ * @private
518
+ */
519
+ mergeCallbacks(...sources) {
520
+ const merged = {};
521
+ const callbackNames = [
522
+ 'onPhaseStart',
523
+ 'onPhaseComplete',
524
+ 'onLayerStart',
525
+ 'onLayerComplete',
526
+ 'onSpawnStart',
527
+ 'onSpawnComplete',
528
+ 'onSpawnOutput',
529
+ 'onError',
530
+ ];
531
+ for (const name of callbackNames) {
532
+ const handlers = sources.map((s) => s[name]).filter(Boolean);
533
+ if (handlers.length > 0) {
534
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
+ ;
536
+ merged[name] = (...args) => {
537
+ for (const handler of handlers) {
538
+ try {
539
+ handler(...args);
540
+ }
541
+ catch (error) {
542
+ // Fire-and-forget: log but don't propagate callback errors
543
+ const errMsg = error instanceof Error ? error.message : String(error);
544
+ this.logger?.warn({ callback: name, error: errMsg }, 'Callback error');
545
+ }
546
+ }
547
+ };
548
+ }
549
+ }
550
+ return merged;
551
+ }
552
+ /**
553
+ * Derive session prefix from input filename
554
+ *
555
+ * Extracts meaningful prefix from input path:
556
+ * - PRD-feature.md → feature
557
+ * - PRD-my-project.md → my-project
558
+ * - epic-001.md → epic-001
559
+ *
560
+ * @param inputPath - Input file path
561
+ * @returns Derived prefix string
562
+ * @private
563
+ */
564
+ derivePrefixFromFilename(inputPath) {
565
+ const filename = basename(inputPath, '.md');
566
+ // Handle PRD-* pattern
567
+ if (filename.startsWith('PRD-')) {
568
+ return filename.slice(4); // Remove 'PRD-' prefix
569
+ }
570
+ // Use filename as-is for other patterns
571
+ return filename;
572
+ }
573
+ /**
574
+ * Display detailed failure information for each phase
575
+ *
576
+ * @param result - Workflow result with all phase data
577
+ * @private
578
+ */
579
+ displayFailureDetails(result) {
580
+ this.log('\n' + colors.error('Failures:'));
581
+ if (result.epicPhase?.failures.length) {
582
+ this.log(colors.error(` Epic Phase:`));
583
+ for (const failure of result.epicPhase.failures) {
584
+ this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
585
+ }
586
+ }
587
+ if (result.storyPhase?.failures.length) {
588
+ this.log(colors.error(` Story Phase:`));
589
+ for (const failure of result.storyPhase.failures) {
590
+ this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
591
+ }
592
+ }
593
+ if (result.devPhase?.failures.length) {
594
+ this.log(colors.error(` Development Phase:`));
595
+ for (const failure of result.devPhase.failures) {
596
+ this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
597
+ }
598
+ }
599
+ if (result.qaPhase?.failures.length) {
600
+ this.log(colors.error(` QA Phase:`));
601
+ for (const failure of result.qaPhase.failures) {
602
+ this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
603
+ }
604
+ }
605
+ }
226
606
  /**
227
607
  * Display dry-run banner
228
608
  *
@@ -345,6 +725,11 @@ export default class Workflow extends Command {
345
725
  if (config.dryRun) {
346
726
  this.log('\n' + colors.warning('No files were created (dry-run mode)'));
347
727
  }
728
+ // Display session directory path (AC: #7)
729
+ if (this.sessionDir) {
730
+ const sessionBanner = formatBox('Session Directory', `${this.sessionDir}\n\nView spawn outputs, session report, and workflow log at this location.`);
731
+ this.log('\n' + colors.info(sessionBanner));
732
+ }
348
733
  }
349
734
  /**
350
735
  * Display phase header with visual separator
@@ -361,17 +746,73 @@ export default class Workflow extends Command {
361
746
  this.log(colors.bold(`║ ${header} ║`));
362
747
  this.log(colors.bold(`╚${separator}╝`) + '\n');
363
748
  }
749
+ /**
750
+ * Finalize session by writing session report and workflow log
751
+ *
752
+ * Called on workflow completion (success or failure).
753
+ * Errors are caught and logged but don't interrupt workflow completion.
754
+ *
755
+ * @param result - Workflow execution result
756
+ * @param _config - Workflow configuration used (unused for now)
757
+ * @private
758
+ */
759
+ async finalizeSession(result, _config) {
760
+ if (!this.sessionDir) {
761
+ this.logger.warn('Session directory not initialized, skipping session finalization');
762
+ return;
763
+ }
764
+ try {
765
+ // Build session report summary from workflow result
766
+ const sessionId = basename(this.sessionDir);
767
+ const now = new Date().toISOString();
768
+ const summary = {
769
+ duration: result.totalDuration,
770
+ endedAt: now,
771
+ errors: this.collectErrors(result),
772
+ overallSuccess: result.overallSuccess,
773
+ phases: {
774
+ developments: {
775
+ completed: result.devPhase?.success ?? 0,
776
+ failed: result.devPhase?.failures.length ?? 0,
777
+ total: (result.devPhase?.success ?? 0) + (result.devPhase?.failures.length ?? 0),
778
+ },
779
+ epics: {
780
+ completed: result.epicPhase?.success ?? 0,
781
+ failed: result.epicPhase?.failures.length ?? 0,
782
+ total: (result.epicPhase?.success ?? 0) + (result.epicPhase?.failures.length ?? 0),
783
+ },
784
+ stories: {
785
+ completed: result.storyPhase?.success ?? 0,
786
+ failed: result.storyPhase?.failures.length ?? 0,
787
+ total: (result.storyPhase?.success ?? 0) + (result.storyPhase?.failures.length ?? 0),
788
+ },
789
+ },
790
+ sessionId,
791
+ spawnFiles: [], // Will be populated by scaffolder internals if needed
792
+ startedAt: new Date(Date.now() - result.totalDuration).toISOString(),
793
+ };
794
+ await this.scaffolder.writeSessionReport(summary);
795
+ this.logger.info({ sessionDir: this.sessionDir }, 'Session report written');
796
+ // Note: workflow-log.yaml would require WorkflowLogger integration
797
+ // which is optional per the orchestrator config. For now, session report
798
+ // is the primary artifact.
799
+ }
800
+ catch (error) {
801
+ this.logger.warn({ error: error.message }, 'Failed to finalize session');
802
+ }
803
+ }
364
804
  /**
365
805
  * Initialize services and dependencies
366
806
  *
367
807
  * Creates all service instances needed for workflow orchestration.
368
808
  * @param maxConcurrency - Maximum number of concurrent operations
369
809
  * @param provider - AI provider to use (claude or gemini)
810
+ * @param verbose - Enable verbose output mode
370
811
  * @private
371
812
  */
372
- async initializeServices(maxConcurrency = 3, provider = 'claude') {
373
- // Create logger
374
- this.logger = createLogger({ namespace: 'commands:workflow' });
813
+ async initializeServices(maxConcurrency = 3, provider = 'claude', verbose = false) {
814
+ // Create logger — suppress INFO logs in non-verbose mode to keep terminal clean
815
+ this.logger = createLogger({ level: verbose ? 'info' : 'warn', namespace: 'commands:workflow' });
375
816
  this.logger.info({ provider }, 'Initializing services with AI provider');
376
817
  // Create file system services
377
818
  const fileManager = new FileManager(this.logger);
@@ -387,10 +828,20 @@ export default class Workflow extends Command {
387
828
  const inputDetector = new InputDetector(fileManager, this.logger);
388
829
  // Create story type detector for auto-documentation
389
830
  const storyTypeDetector = new StoryTypeDetector(this.logger);
390
- // Create orchestrator
831
+ // Create session scaffolder for workflow artifacts
832
+ this.scaffolder = new WorkflowSessionScaffolder(fileManager, this.logger);
833
+ // Create workflow reporter for live progress visualization (AC: #1)
834
+ this.reporter = new WorkflowReporter({
835
+ nonTTY: !process.stdout.isTTY,
836
+ verbose,
837
+ });
838
+ // Merge scaffolder callbacks with reporter callbacks for dual-channel output (AC: #1)
839
+ const callbacks = this.mergeCallbacks(this.createScaffolderCallbacks(), this.reporter.getCallbacks());
840
+ // Create orchestrator with merged callbacks
391
841
  this.orchestrator = new WorkflowOrchestrator({
392
842
  agentRunner,
393
843
  batchProcessor,
844
+ callbacks,
394
845
  epicParser,
395
846
  fileManager,
396
847
  inputDetector,
@@ -399,7 +850,29 @@ export default class Workflow extends Command {
399
850
  prdParser,
400
851
  storyTypeDetector,
401
852
  });
402
- this.logger.info({ provider }, 'Services initialized successfully');
853
+ this.logger.info({ provider, verbose }, 'Services initialized successfully');
854
+ }
855
+ /**
856
+ * Initialize session scaffolder and create session directory structure
857
+ *
858
+ * @param inputPath - Input file path for prefix derivation
859
+ * @param sessionPrefix - Optional explicit session prefix
860
+ * @private
861
+ */
862
+ async initializeSession(inputPath, sessionPrefix) {
863
+ try {
864
+ // Derive prefix: explicit flag > derived from filename
865
+ const prefix = sessionPrefix || this.derivePrefixFromFilename(inputPath);
866
+ this.sessionDir = await this.scaffolder.createSessionStructure({
867
+ baseDir: 'docs/workflow-sessions',
868
+ prefix,
869
+ });
870
+ this.logger.info({ prefix, sessionDir: this.sessionDir }, 'Workflow session initialized');
871
+ }
872
+ catch (error) {
873
+ // Log but don't fail workflow - session is optional (AC: #9)
874
+ this.logger.warn({ error: error.message }, 'Failed to initialize session structure');
875
+ }
403
876
  }
404
877
  /**
405
878
  * Register SIGINT handler for graceful cancellation