@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.
- package/dist/commands/stories/qa.js +29 -73
- package/dist/commands/workflow.d.ts +81 -0
- package/dist/commands/workflow.js +386 -13
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/workflow-callbacks.d.ts +251 -0
- package/dist/models/workflow-callbacks.js +10 -0
- package/dist/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +6 -1
- package/dist/services/orchestration/workflow-orchestrator.d.ts +33 -1
- package/dist/services/orchestration/workflow-orchestrator.js +869 -275
- package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- package/dist/utils/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/package.json +3 -2
|
@@ -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.
|
|
129
|
+
this.logger.info({ pattern: args.pattern }, 'No story files matched pattern');
|
|
131
130
|
return;
|
|
132
131
|
}
|
|
133
|
-
this.
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
429
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
*
|