@hyperdrive.bot/bmad-workflow 1.0.16 → 1.0.18

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.
@@ -22,7 +22,6 @@ import { PathResolver } from '../../services/file-system/path-resolver.js';
22
22
  import { StoryParserFactory } from '../../services/parsers/story-parser-factory.js';
23
23
  import * as colors from '../../utils/colors.js';
24
24
  import { createLogger, generateCorrelationId } from '../../utils/logger.js';
25
- import { createSpinner } from '../../utils/progress.js';
26
25
  import { runAgentWithRetry } from '../../utils/retry.js';
27
26
  /**
28
27
  * Stories QA Command
@@ -127,11 +126,10 @@ export default class StoriesQaCommand extends Command {
127
126
  // Match story files using glob pattern
128
127
  const storyFiles = await this.matchStoryFiles(args.pattern);
129
128
  if (storyFiles.length === 0) {
130
- this.log(colors.warning(`No story files matched pattern: ${args.pattern}`));
129
+ this.logger.info({ pattern: args.pattern }, 'No story files matched pattern');
131
130
  return;
132
131
  }
133
- this.log(colors.info(`Found ${storyFiles.length} story file(s) to QA`));
134
- this.log('');
132
+ this.logger.info({ count: storyFiles.length }, 'Found story file(s) to QA');
135
133
  // Process stories sequentially
136
134
  const results = await this.qaStoriesSequentially(storyFiles, flags);
137
135
  // Display summary report
@@ -245,13 +243,8 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
245
243
  * Display countdown timer between stories
246
244
  */
247
245
  async displayCountdown(intervalSeconds) {
248
- /* eslint-disable no-await-in-loop */
249
- for (let remaining = intervalSeconds; remaining > 0; remaining--) {
250
- process.stdout.write(`\r${colors.warning(`⏳ Next story in ${remaining}s...`)}`);
251
- await this.sleep(1000);
252
- }
253
- /* eslint-enable no-await-in-loop */
254
- process.stdout.write('\r' + ' '.repeat(40) + '\r'); // Clear countdown line
246
+ this.logger.info({ seconds: intervalSeconds }, 'Waiting before next story');
247
+ await this.sleep(intervalSeconds * 1000);
255
248
  }
256
249
  /**
257
250
  * Display summary report
@@ -264,45 +257,22 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
264
257
  const errorCount = results.filter((r) => !r.success).length;
265
258
  const movedToDone = results.filter((r) => r.movedTo === 'done').length;
266
259
  const movedBack = results.filter((r) => r.movedTo === 'stories').length;
267
- // Box drawing
268
- const boxTop = '┌─────────────────────────────────────────┐';
269
- const boxDivider = '├─────────────────────────────────────────┤';
270
- const boxBottom = '└─────────────────────────────────────────┘';
271
- this.log('');
272
- this.log(boxTop);
273
- this.log('│ Story QA Summary │');
274
- this.log(boxDivider);
275
- this.log(`│ ${colors.success('PASS:')} ${passCount.toString().padEnd(20)}│`);
276
- if (concernsCount > 0) {
277
- this.log(`│ ${colors.warning('CONCERNS:')} ${concernsCount.toString().padEnd(20)}│`);
278
- }
279
- if (failCount > 0) {
280
- this.log(`│ ${colors.error('FAIL:')} ${failCount.toString().padEnd(20)}│`);
281
- }
282
- if (waivedCount > 0) {
283
- this.log(`│ WAIVED: ${waivedCount.toString().padEnd(20)}│`);
284
- }
285
- if (errorCount > 0) {
286
- this.log(`│ ${colors.error('Errors:')} ${errorCount.toString().padEnd(20)}│`);
287
- }
288
- this.log(boxDivider);
289
- this.log(`│ Moved to Done: ${movedToDone.toString().padEnd(20)}│`);
290
- this.log(`│ Returned to Dev: ${movedBack.toString().padEnd(20)}│`);
291
- this.log(boxDivider);
292
- this.log(`│ Duration: ${(duration / 1000).toFixed(2)}s${' '.repeat(15)}│`);
293
- this.log(boxBottom);
294
- // List details for non-PASS stories
295
260
  const nonPassStories = results.filter((r) => r.finalGate !== 'PASS' || !r.success);
296
- if (nonPassStories.length > 0) {
297
- this.log('');
298
- this.log(colors.bold('Stories Requiring Attention:'));
299
- for (const result of nonPassStories) {
300
- const status = result.success ? result.finalGate : 'ERROR';
301
- const statusColor = status === 'CONCERNS' ? colors.warning : colors.error;
302
- this.log(statusColor(` ${status}: ${result.storyNumber} - ${path.basename(result.storyPath)}${result.error ? ` (${result.error})` : ''}`));
303
- }
304
- }
305
- this.log('');
261
+ this.logger.info({
262
+ concerns: concernsCount,
263
+ durationSec: (duration / 1000).toFixed(2),
264
+ errors: errorCount,
265
+ fail: failCount,
266
+ movedBack,
267
+ movedToDone,
268
+ nonPass: nonPassStories.map((r) => ({
269
+ error: r.error,
270
+ gate: r.success ? r.finalGate : 'ERROR',
271
+ story: r.storyNumber,
272
+ })),
273
+ pass: passCount,
274
+ waived: waivedCount,
275
+ }, 'Story QA Summary');
306
276
  }
307
277
  /**
308
278
  * Extract gate status from story content or gate file
@@ -393,19 +363,16 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
393
363
  for (let index = 0; index < storyFiles.length; index++) {
394
364
  const storyPath = storyFiles[index];
395
365
  const storyNum = index + 1;
396
- this.log(colors.bold(`\n[${storyNum}/${total}] QA: ${path.basename(storyPath)}`));
366
+ this.logger.info({ storyNum, total, story: path.basename(storyPath) }, 'QA starting');
397
367
  // Parse story metadata
398
- const spinner = createSpinner('Parsing story metadata...');
399
- spinner.start();
400
368
  let storyMetadata;
401
369
  try {
402
370
  storyMetadata = await this.storyParserFactory.parseStory(storyPath);
403
- const storyId = storyMetadata.id;
404
- spinner.succeed(colors.success(`Story ${storyId}: ${storyMetadata.title}`));
371
+ this.logger.info({ storyId: storyMetadata.id, title: storyMetadata.title }, 'Parsed story');
405
372
  }
406
373
  catch (error) {
407
374
  const err = error;
408
- spinner.fail(colors.error(`Failed to parse story: ${err.message}`));
375
+ this.logger.error({ error: err.message, storyPath }, 'Failed to parse story');
409
376
  results.push({
410
377
  error: `Parse error: ${err.message}`,
411
378
  finalGate: 'UNKNOWN',
@@ -425,19 +392,8 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
425
392
  reference: flags.reference,
426
393
  });
427
394
  results.push(result);
428
- // Display result
429
- if (result.finalGate === 'PASS') {
430
- this.log(colors.success(` ✓ PASSED - Moved to done`));
431
- }
432
- else if (result.finalGate === 'WAIVED') {
433
- this.log(colors.warning(` ⚠ WAIVED - Moved to done`));
434
- }
435
- else if (result.success) {
436
- this.log(colors.warning(` ⚠ ${result.finalGate} - Returned for rework`));
437
- }
438
- else {
439
- this.log(colors.error(` ✗ Error: ${result.error}`));
440
- }
395
+ // Log result
396
+ this.logger.info({ gate: result.finalGate, movedTo: result.movedTo, story: path.basename(storyPath), success: result.success }, result.success ? `QA ${result.finalGate}` : `QA error: ${result.error}`);
441
397
  // Display countdown timer before next story (except for last story)
442
398
  if (index < storyFiles.length - 1) {
443
399
  await this.displayCountdown(flags.interval);
@@ -456,7 +412,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
456
412
  this.logger.info({ storyNumber, storyPath }, 'Starting story QA workflow');
457
413
  try {
458
414
  // Phase 1: Initial QA Deep Dive
459
- this.log(colors.info(' Phase 1: QA Deep Dive Review...'));
415
+ this.logger.info('Phase 1: QA Deep Dive Review');
460
416
  const agentTimeout = flags.timeout ?? 2_700_000;
461
417
  const agentRetries = flags['agent-retries'];
462
418
  const retryBackoff = flags['retry-backoff'];
@@ -475,11 +431,11 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
475
431
  // Read story to get gate status
476
432
  let storyContent = await this.fileManager.readFile(storyPath);
477
433
  currentGate = this.extractGateStatus(storyContent);
478
- this.log(colors.info(` Initial Gate: ${currentGate}`));
434
+ this.logger.info({ gate: currentGate }, 'Initial gate result');
479
435
  // Phase 2: Dev Fix-Forward Loop (if needed and retries allowed)
480
436
  while ((currentGate === 'CONCERNS' || currentGate === 'FAIL') && retriesUsed < flags['max-retries']) {
481
437
  retriesUsed++;
482
- this.log(colors.warning(` Phase 2: Dev Fix-Forward (Retry ${retriesUsed}/${flags['max-retries']})...`));
438
+ this.logger.info({ retry: retriesUsed, maxRetries: flags['max-retries'] }, 'Phase 2: Dev Fix-Forward');
483
439
  // Run Dev agent to fix issues (sequential retry loop by design)
484
440
  const devPrompt = this.buildDevFixPrompt(storyPath, flags['dev-prompt'], flags.reference);
485
441
  // eslint-disable-next-line no-await-in-loop
@@ -496,7 +452,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
496
452
  // Continue anyway - QA will re-evaluate
497
453
  }
498
454
  // Phase 3: Re-run QA to validate fixes
499
- this.log(colors.info(` Phase 3: QA Re-validation (Retry ${retriesUsed})...`));
455
+ this.logger.info({ retry: retriesUsed }, 'Phase 3: QA Re-validation');
500
456
  // eslint-disable-next-line no-await-in-loop
501
457
  const reQaResult = await runAgentWithRetry(this.agentRunner, qaPrompt, {
502
458
  agentType: 'tea',
@@ -513,7 +469,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
513
469
  // eslint-disable-next-line no-await-in-loop
514
470
  storyContent = await this.fileManager.readFile(storyPath);
515
471
  currentGate = this.extractGateStatus(storyContent);
516
- this.log(colors.info(` Gate after retry ${retriesUsed}: ${currentGate}`));
472
+ this.logger.info({ gate: currentGate, retry: retriesUsed }, 'Gate after retry');
517
473
  }
518
474
  // Append QA workflow summary to story
519
475
  const result = {
@@ -33,6 +33,7 @@ export default class Workflow extends Command {
33
33
  'prd-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
34
34
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
35
35
  prefix: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
36
+ 'session-prefix': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
36
37
  provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
37
38
  qa: import("@oclif/core/interfaces").BooleanFlag<boolean>;
38
39
  'qa-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -50,12 +51,72 @@ export default class Workflow extends Command {
50
51
  private cancelled;
51
52
  private logger;
52
53
  private orchestrator;
54
+ private reporter;
55
+ private scaffolder;
56
+ private sessionDir;
53
57
  /**
54
58
  * Main command execution
55
59
  *
56
60
  * Orchestrates the complete workflow with progress tracking and summary display.
57
61
  */
58
62
  run(): Promise<void>;
63
+ /**
64
+ * Collect all errors from workflow result into a flat list
65
+ *
66
+ * @param result - Workflow execution result
67
+ * @returns Array of error entries with phase context
68
+ * @private
69
+ */
70
+ private collectErrors;
71
+ /**
72
+ * Build dashboard summary from workflow result for final display
73
+ *
74
+ * @param result - Workflow execution result
75
+ * @returns DashboardSummary for the reporter's final dashboard
76
+ * @private
77
+ */
78
+ private buildDashboardSummary;
79
+ /**
80
+ * Create callbacks object wired to scaffolder for incremental artifact writing
81
+ *
82
+ * All callbacks are wrapped in try-catch to prevent scaffolder errors from
83
+ * interrupting workflow execution (AC: #9).
84
+ * @returns WorkflowCallbacks object with scaffolder integration
85
+ * @private
86
+ */
87
+ private createScaffolderCallbacks;
88
+ /**
89
+ * Merge multiple callback objects into a single callbacks object
90
+ *
91
+ * When the same callback is defined in multiple sources, all handlers are called
92
+ * in sequence (fire-and-forget). This enables dual-channel output where scaffolder
93
+ * and reporter receive events independently.
94
+ *
95
+ * @param sources - Array of WorkflowCallbacks objects to merge
96
+ * @returns Merged WorkflowCallbacks object
97
+ * @private
98
+ */
99
+ private mergeCallbacks;
100
+ /**
101
+ * Derive session prefix from input filename
102
+ *
103
+ * Extracts meaningful prefix from input path:
104
+ * - PRD-feature.md → feature
105
+ * - PRD-my-project.md → my-project
106
+ * - epic-001.md → epic-001
107
+ *
108
+ * @param inputPath - Input file path
109
+ * @returns Derived prefix string
110
+ * @private
111
+ */
112
+ private derivePrefixFromFilename;
113
+ /**
114
+ * Display detailed failure information for each phase
115
+ *
116
+ * @param result - Workflow result with all phase data
117
+ * @private
118
+ */
119
+ private displayFailureDetails;
59
120
  /**
60
121
  * Display dry-run banner
61
122
  *
@@ -82,15 +143,35 @@ export default class Workflow extends Command {
82
143
  * @private
83
144
  */
84
145
  private displayPhaseHeader;
146
+ /**
147
+ * Finalize session by writing session report and workflow log
148
+ *
149
+ * Called on workflow completion (success or failure).
150
+ * Errors are caught and logged but don't interrupt workflow completion.
151
+ *
152
+ * @param result - Workflow execution result
153
+ * @param _config - Workflow configuration used (unused for now)
154
+ * @private
155
+ */
156
+ private finalizeSession;
85
157
  /**
86
158
  * Initialize services and dependencies
87
159
  *
88
160
  * Creates all service instances needed for workflow orchestration.
89
161
  * @param maxConcurrency - Maximum number of concurrent operations
90
162
  * @param provider - AI provider to use (claude or gemini)
163
+ * @param verbose - Enable verbose output mode
91
164
  * @private
92
165
  */
93
166
  private initializeServices;
167
+ /**
168
+ * Initialize session scaffolder and create session directory structure
169
+ *
170
+ * @param inputPath - Input file path for prefix derivation
171
+ * @param sessionPrefix - Optional explicit session prefix
172
+ * @private
173
+ */
174
+ private initializeSession;
94
175
  /**
95
176
  * Register SIGINT handler for graceful cancellation
96
177
  *