@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { basename } from 'node:path';
|
|
2
3
|
import { createAgentRunner, isProviderSupported } from '../services/agents/agent-runner-factory.js';
|
|
3
4
|
import { FileManager } from '../services/file-system/file-manager.js';
|
|
4
5
|
import { PathResolver } from '../services/file-system/path-resolver.js';
|
|
@@ -8,6 +9,8 @@ import { StoryTypeDetector } from '../services/orchestration/story-type-detector
|
|
|
8
9
|
import { WorkflowOrchestrator } from '../services/orchestration/workflow-orchestrator.js';
|
|
9
10
|
import { EpicParser } from '../services/parsers/epic-parser.js';
|
|
10
11
|
import { PrdParser } from '../services/parsers/prd-parser.js';
|
|
12
|
+
import { WorkflowSessionScaffolder } from '../services/scaffolding/workflow-session-scaffolder.js';
|
|
13
|
+
import { WorkflowReporter } from '../services/WorkflowReporter.js';
|
|
11
14
|
import * as colors from '../utils/colors.js';
|
|
12
15
|
import { formatBox, formatTable } from '../utils/formatters.js';
|
|
13
16
|
import { createLogger } from '../utils/logger.js';
|
|
@@ -77,7 +80,10 @@ export default class Workflow extends Command {
|
|
|
77
80
|
}),
|
|
78
81
|
prefix: Flags.string({
|
|
79
82
|
default: '',
|
|
80
|
-
description: 'Filename prefix for generated files',
|
|
83
|
+
description: 'Filename prefix for generated files and session directory name',
|
|
84
|
+
}),
|
|
85
|
+
'session-prefix': Flags.string({
|
|
86
|
+
description: 'Session directory prefix (default: derived from input filename, e.g., PRD-feature.md → feature)',
|
|
81
87
|
}),
|
|
82
88
|
provider: Flags.string({
|
|
83
89
|
default: 'claude',
|
|
@@ -133,12 +139,15 @@ export default class Workflow extends Command {
|
|
|
133
139
|
verbose: Flags.boolean({
|
|
134
140
|
char: 'v',
|
|
135
141
|
default: false,
|
|
136
|
-
description: '
|
|
142
|
+
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
143
|
}),
|
|
138
144
|
};
|
|
139
145
|
cancelled = false;
|
|
140
146
|
logger;
|
|
141
147
|
orchestrator;
|
|
148
|
+
reporter;
|
|
149
|
+
scaffolder;
|
|
150
|
+
sessionDir = null;
|
|
142
151
|
/**
|
|
143
152
|
* Main command execution
|
|
144
153
|
*
|
|
@@ -147,14 +156,24 @@ export default class Workflow extends Command {
|
|
|
147
156
|
async run() {
|
|
148
157
|
const { args, flags } = await this.parse(Workflow);
|
|
149
158
|
try {
|
|
159
|
+
// Early validation: check input file exists before initializing anything
|
|
160
|
+
const inputPath = args.input;
|
|
161
|
+
if (!inputPath.includes('*') && !inputPath.includes('?')) {
|
|
162
|
+
const { existsSync } = await import('node:fs');
|
|
163
|
+
if (!existsSync(inputPath)) {
|
|
164
|
+
this.error(`File not found: ${inputPath}`, { exit: 1 });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
150
167
|
// Validate provider
|
|
151
168
|
if (!isProviderSupported(flags.provider)) {
|
|
152
169
|
this.error(`Unsupported provider: ${flags.provider}. Use 'claude', 'gemini', or 'opencode'.`, { exit: 1 });
|
|
153
170
|
}
|
|
154
171
|
// Initialize services with parallel concurrency and provider
|
|
155
|
-
await this.initializeServices(flags.parallel, flags.provider);
|
|
172
|
+
await this.initializeServices(flags.parallel, flags.provider, flags.verbose);
|
|
156
173
|
// Register signal handlers
|
|
157
174
|
this.registerSignalHandlers();
|
|
175
|
+
// Initialize session scaffolder and create session structure
|
|
176
|
+
await this.initializeSession(args.input, flags['session-prefix']);
|
|
158
177
|
// Show dry-run banner if applicable
|
|
159
178
|
if (flags['dry-run']) {
|
|
160
179
|
this.displayDryRunBanner();
|
|
@@ -193,28 +212,289 @@ export default class Workflow extends Command {
|
|
|
193
212
|
// Execute workflow
|
|
194
213
|
this.log(colors.info('\nStarting workflow orchestration...\n'));
|
|
195
214
|
const result = await this.orchestrator.execute(config);
|
|
215
|
+
// Wait for reporter listr2 tasks to complete and clean up
|
|
216
|
+
await this.reporter?.waitForCompletion();
|
|
217
|
+
// Write session report on completion (includes cancellation case)
|
|
218
|
+
await this.finalizeSession(result, config);
|
|
219
|
+
// Build dashboard summary from workflow result
|
|
220
|
+
const dashboardSummary = this.buildDashboardSummary(result);
|
|
221
|
+
// Display final dashboard using reporter (AC: #4)
|
|
222
|
+
this.reporter?.displayFinalDashboard(dashboardSummary);
|
|
223
|
+
this.reporter?.dispose();
|
|
224
|
+
// Display failure details if any
|
|
225
|
+
if (result.totalFailures > 0) {
|
|
226
|
+
this.displayFailureDetails(result);
|
|
227
|
+
}
|
|
228
|
+
// Dry run reminder
|
|
229
|
+
if (config.dryRun) {
|
|
230
|
+
this.log('\n' + colors.warning('No files were created (dry-run mode)'));
|
|
231
|
+
}
|
|
196
232
|
// Check for cancellation
|
|
197
233
|
if (this.cancelled) {
|
|
198
234
|
this.log('\n' + colors.warning('Workflow cancelled by user'));
|
|
199
|
-
this.displayFinalSummary(result, config);
|
|
200
235
|
process.exit(130); // Standard interrupted exit code
|
|
201
236
|
}
|
|
202
|
-
// Display final summary
|
|
203
|
-
this.displayFinalSummary(result, config);
|
|
204
237
|
// Exit with appropriate code
|
|
205
238
|
if (!result.overallSuccess) {
|
|
206
239
|
this.error('Workflow completed with failures', { exit: 1 });
|
|
207
240
|
}
|
|
208
|
-
this.log('\n' + colors.success('
|
|
241
|
+
this.log('\n' + colors.success('Workflow completed successfully!'));
|
|
209
242
|
}
|
|
210
243
|
catch (error) {
|
|
211
|
-
|
|
244
|
+
// Clean up reporter on error
|
|
245
|
+
this.reporter?.dispose();
|
|
246
|
+
this.logger?.error({ error: error.message }, 'Workflow command failed');
|
|
212
247
|
// Display user-friendly error
|
|
213
248
|
this.log('\n' + colors.error('✗ Workflow failed:'));
|
|
214
249
|
this.log(colors.error(` ${error.message}`));
|
|
215
250
|
this.error('Workflow execution failed', { exit: 1 });
|
|
216
251
|
}
|
|
217
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Collect all errors from workflow result into a flat list
|
|
255
|
+
*
|
|
256
|
+
* @param result - Workflow execution result
|
|
257
|
+
* @returns Array of error entries with phase context
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
collectErrors(result) {
|
|
261
|
+
const errors = [];
|
|
262
|
+
const now = new Date().toISOString();
|
|
263
|
+
if (result.epicPhase?.failures) {
|
|
264
|
+
for (const failure of result.epicPhase.failures) {
|
|
265
|
+
errors.push({
|
|
266
|
+
error: failure.error,
|
|
267
|
+
identifier: failure.identifier,
|
|
268
|
+
phase: 'epic',
|
|
269
|
+
timestamp: now,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (result.storyPhase?.failures) {
|
|
274
|
+
for (const failure of result.storyPhase.failures) {
|
|
275
|
+
errors.push({
|
|
276
|
+
error: failure.error,
|
|
277
|
+
identifier: failure.identifier,
|
|
278
|
+
phase: 'story',
|
|
279
|
+
timestamp: now,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (result.devPhase?.failures) {
|
|
284
|
+
for (const failure of result.devPhase.failures) {
|
|
285
|
+
errors.push({
|
|
286
|
+
error: failure.error,
|
|
287
|
+
identifier: failure.identifier,
|
|
288
|
+
phase: 'dev',
|
|
289
|
+
timestamp: now,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (result.qaPhase?.failures) {
|
|
294
|
+
for (const failure of result.qaPhase.failures) {
|
|
295
|
+
errors.push({
|
|
296
|
+
error: failure.error,
|
|
297
|
+
identifier: failure.identifier,
|
|
298
|
+
phase: 'qa',
|
|
299
|
+
timestamp: now,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return errors;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Build dashboard summary from workflow result for final display
|
|
307
|
+
*
|
|
308
|
+
* @param result - Workflow execution result
|
|
309
|
+
* @returns DashboardSummary for the reporter's final dashboard
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
buildDashboardSummary(result) {
|
|
313
|
+
const phases = [];
|
|
314
|
+
// Add phases in order: epic, story, dev, qa
|
|
315
|
+
if (result.epicPhase && !result.epicPhase.skipped) {
|
|
316
|
+
phases.push({
|
|
317
|
+
duration: result.epicPhase.duration,
|
|
318
|
+
failed: result.epicPhase.failures.length,
|
|
319
|
+
name: 'epic',
|
|
320
|
+
passed: result.epicPhase.success,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (result.storyPhase && !result.storyPhase.skipped) {
|
|
324
|
+
phases.push({
|
|
325
|
+
duration: result.storyPhase.duration,
|
|
326
|
+
failed: result.storyPhase.failures.length,
|
|
327
|
+
name: 'story',
|
|
328
|
+
passed: result.storyPhase.success,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (result.devPhase && !result.devPhase.skipped) {
|
|
332
|
+
phases.push({
|
|
333
|
+
duration: result.devPhase.duration,
|
|
334
|
+
failed: result.devPhase.failures.length,
|
|
335
|
+
name: 'dev',
|
|
336
|
+
passed: result.devPhase.success,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (result.qaPhase && !result.qaPhase.skipped) {
|
|
340
|
+
phases.push({
|
|
341
|
+
duration: result.qaPhase.duration,
|
|
342
|
+
failed: result.qaPhase.failures.length,
|
|
343
|
+
name: 'qa',
|
|
344
|
+
passed: result.qaPhase.success,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
// Build session report path from session directory
|
|
348
|
+
const sessionReportPath = this.sessionDir ? `${this.sessionDir}/SESSION_REPORT.md` : undefined;
|
|
349
|
+
return {
|
|
350
|
+
failedCount: result.totalFailures,
|
|
351
|
+
passedCount: result.totalFilesProcessed - result.totalFailures,
|
|
352
|
+
phases,
|
|
353
|
+
sessionReportPath,
|
|
354
|
+
totalDuration: result.totalDuration,
|
|
355
|
+
totalSpawns: result.totalFilesProcessed,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Create callbacks object wired to scaffolder for incremental artifact writing
|
|
360
|
+
*
|
|
361
|
+
* All callbacks are wrapped in try-catch to prevent scaffolder errors from
|
|
362
|
+
* interrupting workflow execution (AC: #9).
|
|
363
|
+
* @returns WorkflowCallbacks object with scaffolder integration
|
|
364
|
+
* @private
|
|
365
|
+
*/
|
|
366
|
+
createScaffolderCallbacks() {
|
|
367
|
+
return {
|
|
368
|
+
onSpawnComplete: (context) => {
|
|
369
|
+
// Write spawn output immediately for crash resilience (AC: #8)
|
|
370
|
+
try {
|
|
371
|
+
// Map phaseName to spawn type
|
|
372
|
+
const typeMap = {
|
|
373
|
+
dev: 'dev',
|
|
374
|
+
epic: 'epic',
|
|
375
|
+
story: 'story',
|
|
376
|
+
};
|
|
377
|
+
const spawnType = typeMap[context.phaseName] || 'dev';
|
|
378
|
+
// Fire-and-forget write to not block workflow
|
|
379
|
+
this.scaffolder
|
|
380
|
+
.writeSpawnOutput({
|
|
381
|
+
completedAt: context.endTime ? new Date(context.endTime).toISOString() : undefined,
|
|
382
|
+
content: context.output || '',
|
|
383
|
+
duration: context.duration,
|
|
384
|
+
errors: context.error,
|
|
385
|
+
spawnId: context.spawnId,
|
|
386
|
+
success: context.success,
|
|
387
|
+
type: spawnType,
|
|
388
|
+
})
|
|
389
|
+
.catch((writeError) => {
|
|
390
|
+
this.logger.warn({ error: writeError.message, spawnId: context.spawnId }, 'Async spawn output write failed');
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
// Log but don't propagate - workflow must continue (AC: #9)
|
|
395
|
+
this.logger.warn({ error: error.message, spawnId: context.spawnId }, 'Failed to write spawn output');
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Merge multiple callback objects into a single callbacks object
|
|
402
|
+
*
|
|
403
|
+
* When the same callback is defined in multiple sources, all handlers are called
|
|
404
|
+
* in sequence (fire-and-forget). This enables dual-channel output where scaffolder
|
|
405
|
+
* and reporter receive events independently.
|
|
406
|
+
*
|
|
407
|
+
* @param sources - Array of WorkflowCallbacks objects to merge
|
|
408
|
+
* @returns Merged WorkflowCallbacks object
|
|
409
|
+
* @private
|
|
410
|
+
*/
|
|
411
|
+
mergeCallbacks(...sources) {
|
|
412
|
+
const merged = {};
|
|
413
|
+
const callbackNames = [
|
|
414
|
+
'onPhaseStart',
|
|
415
|
+
'onPhaseComplete',
|
|
416
|
+
'onLayerStart',
|
|
417
|
+
'onLayerComplete',
|
|
418
|
+
'onSpawnStart',
|
|
419
|
+
'onSpawnComplete',
|
|
420
|
+
'onSpawnOutput',
|
|
421
|
+
'onError',
|
|
422
|
+
];
|
|
423
|
+
for (const name of callbackNames) {
|
|
424
|
+
const handlers = sources.map((s) => s[name]).filter(Boolean);
|
|
425
|
+
if (handlers.length > 0) {
|
|
426
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
427
|
+
;
|
|
428
|
+
merged[name] = (...args) => {
|
|
429
|
+
for (const handler of handlers) {
|
|
430
|
+
try {
|
|
431
|
+
handler(...args);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
// Fire-and-forget: log but don't propagate callback errors
|
|
435
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
436
|
+
this.logger?.warn({ callback: name, error: errMsg }, 'Callback error');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return merged;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Derive session prefix from input filename
|
|
446
|
+
*
|
|
447
|
+
* Extracts meaningful prefix from input path:
|
|
448
|
+
* - PRD-feature.md → feature
|
|
449
|
+
* - PRD-my-project.md → my-project
|
|
450
|
+
* - epic-001.md → epic-001
|
|
451
|
+
*
|
|
452
|
+
* @param inputPath - Input file path
|
|
453
|
+
* @returns Derived prefix string
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
derivePrefixFromFilename(inputPath) {
|
|
457
|
+
const filename = basename(inputPath, '.md');
|
|
458
|
+
// Handle PRD-* pattern
|
|
459
|
+
if (filename.startsWith('PRD-')) {
|
|
460
|
+
return filename.slice(4); // Remove 'PRD-' prefix
|
|
461
|
+
}
|
|
462
|
+
// Use filename as-is for other patterns
|
|
463
|
+
return filename;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Display detailed failure information for each phase
|
|
467
|
+
*
|
|
468
|
+
* @param result - Workflow result with all phase data
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
displayFailureDetails(result) {
|
|
472
|
+
this.log('\n' + colors.error('Failures:'));
|
|
473
|
+
if (result.epicPhase?.failures.length) {
|
|
474
|
+
this.log(colors.error(` Epic Phase:`));
|
|
475
|
+
for (const failure of result.epicPhase.failures) {
|
|
476
|
+
this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (result.storyPhase?.failures.length) {
|
|
480
|
+
this.log(colors.error(` Story Phase:`));
|
|
481
|
+
for (const failure of result.storyPhase.failures) {
|
|
482
|
+
this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (result.devPhase?.failures.length) {
|
|
486
|
+
this.log(colors.error(` Development Phase:`));
|
|
487
|
+
for (const failure of result.devPhase.failures) {
|
|
488
|
+
this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (result.qaPhase?.failures.length) {
|
|
492
|
+
this.log(colors.error(` QA Phase:`));
|
|
493
|
+
for (const failure of result.qaPhase.failures) {
|
|
494
|
+
this.log(colors.error(` • ${failure.identifier}: ${failure.error}`));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
218
498
|
/**
|
|
219
499
|
* Display dry-run banner
|
|
220
500
|
*
|
|
@@ -337,6 +617,11 @@ export default class Workflow extends Command {
|
|
|
337
617
|
if (config.dryRun) {
|
|
338
618
|
this.log('\n' + colors.warning('No files were created (dry-run mode)'));
|
|
339
619
|
}
|
|
620
|
+
// Display session directory path (AC: #7)
|
|
621
|
+
if (this.sessionDir) {
|
|
622
|
+
const sessionBanner = formatBox('Session Directory', `${this.sessionDir}\n\nView spawn outputs, session report, and workflow log at this location.`);
|
|
623
|
+
this.log('\n' + colors.info(sessionBanner));
|
|
624
|
+
}
|
|
340
625
|
}
|
|
341
626
|
/**
|
|
342
627
|
* Display phase header with visual separator
|
|
@@ -353,17 +638,73 @@ export default class Workflow extends Command {
|
|
|
353
638
|
this.log(colors.bold(`║ ${header} ║`));
|
|
354
639
|
this.log(colors.bold(`╚${separator}╝`) + '\n');
|
|
355
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Finalize session by writing session report and workflow log
|
|
643
|
+
*
|
|
644
|
+
* Called on workflow completion (success or failure).
|
|
645
|
+
* Errors are caught and logged but don't interrupt workflow completion.
|
|
646
|
+
*
|
|
647
|
+
* @param result - Workflow execution result
|
|
648
|
+
* @param _config - Workflow configuration used (unused for now)
|
|
649
|
+
* @private
|
|
650
|
+
*/
|
|
651
|
+
async finalizeSession(result, _config) {
|
|
652
|
+
if (!this.sessionDir) {
|
|
653
|
+
this.logger.warn('Session directory not initialized, skipping session finalization');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
// Build session report summary from workflow result
|
|
658
|
+
const sessionId = basename(this.sessionDir);
|
|
659
|
+
const now = new Date().toISOString();
|
|
660
|
+
const summary = {
|
|
661
|
+
duration: result.totalDuration,
|
|
662
|
+
endedAt: now,
|
|
663
|
+
errors: this.collectErrors(result),
|
|
664
|
+
overallSuccess: result.overallSuccess,
|
|
665
|
+
phases: {
|
|
666
|
+
developments: {
|
|
667
|
+
completed: result.devPhase?.success ?? 0,
|
|
668
|
+
failed: result.devPhase?.failures.length ?? 0,
|
|
669
|
+
total: (result.devPhase?.success ?? 0) + (result.devPhase?.failures.length ?? 0),
|
|
670
|
+
},
|
|
671
|
+
epics: {
|
|
672
|
+
completed: result.epicPhase?.success ?? 0,
|
|
673
|
+
failed: result.epicPhase?.failures.length ?? 0,
|
|
674
|
+
total: (result.epicPhase?.success ?? 0) + (result.epicPhase?.failures.length ?? 0),
|
|
675
|
+
},
|
|
676
|
+
stories: {
|
|
677
|
+
completed: result.storyPhase?.success ?? 0,
|
|
678
|
+
failed: result.storyPhase?.failures.length ?? 0,
|
|
679
|
+
total: (result.storyPhase?.success ?? 0) + (result.storyPhase?.failures.length ?? 0),
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
sessionId,
|
|
683
|
+
spawnFiles: [], // Will be populated by scaffolder internals if needed
|
|
684
|
+
startedAt: new Date(Date.now() - result.totalDuration).toISOString(),
|
|
685
|
+
};
|
|
686
|
+
await this.scaffolder.writeSessionReport(summary);
|
|
687
|
+
this.logger.info({ sessionDir: this.sessionDir }, 'Session report written');
|
|
688
|
+
// Note: workflow-log.yaml would require WorkflowLogger integration
|
|
689
|
+
// which is optional per the orchestrator config. For now, session report
|
|
690
|
+
// is the primary artifact.
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
this.logger.warn({ error: error.message }, 'Failed to finalize session');
|
|
694
|
+
}
|
|
695
|
+
}
|
|
356
696
|
/**
|
|
357
697
|
* Initialize services and dependencies
|
|
358
698
|
*
|
|
359
699
|
* Creates all service instances needed for workflow orchestration.
|
|
360
700
|
* @param maxConcurrency - Maximum number of concurrent operations
|
|
361
701
|
* @param provider - AI provider to use (claude or gemini)
|
|
702
|
+
* @param verbose - Enable verbose output mode
|
|
362
703
|
* @private
|
|
363
704
|
*/
|
|
364
|
-
async initializeServices(maxConcurrency = 3, provider = 'claude') {
|
|
365
|
-
// Create logger
|
|
366
|
-
this.logger = createLogger({ namespace: 'commands:workflow' });
|
|
705
|
+
async initializeServices(maxConcurrency = 3, provider = 'claude', verbose = false) {
|
|
706
|
+
// Create logger — suppress INFO logs in non-verbose mode to keep terminal clean
|
|
707
|
+
this.logger = createLogger({ level: verbose ? 'info' : 'warn', namespace: 'commands:workflow' });
|
|
367
708
|
this.logger.info({ provider }, 'Initializing services with AI provider');
|
|
368
709
|
// Create file system services
|
|
369
710
|
const fileManager = new FileManager(this.logger);
|
|
@@ -379,10 +720,20 @@ export default class Workflow extends Command {
|
|
|
379
720
|
const inputDetector = new InputDetector(fileManager, this.logger);
|
|
380
721
|
// Create story type detector for auto-documentation
|
|
381
722
|
const storyTypeDetector = new StoryTypeDetector(this.logger);
|
|
382
|
-
// Create
|
|
723
|
+
// Create session scaffolder for workflow artifacts
|
|
724
|
+
this.scaffolder = new WorkflowSessionScaffolder(fileManager, this.logger);
|
|
725
|
+
// Create workflow reporter for live progress visualization (AC: #1)
|
|
726
|
+
this.reporter = new WorkflowReporter({
|
|
727
|
+
nonTTY: !process.stdout.isTTY,
|
|
728
|
+
verbose,
|
|
729
|
+
});
|
|
730
|
+
// Merge scaffolder callbacks with reporter callbacks for dual-channel output (AC: #1)
|
|
731
|
+
const callbacks = this.mergeCallbacks(this.createScaffolderCallbacks(), this.reporter.getCallbacks());
|
|
732
|
+
// Create orchestrator with merged callbacks
|
|
383
733
|
this.orchestrator = new WorkflowOrchestrator({
|
|
384
734
|
agentRunner,
|
|
385
735
|
batchProcessor,
|
|
736
|
+
callbacks,
|
|
386
737
|
epicParser,
|
|
387
738
|
fileManager,
|
|
388
739
|
inputDetector,
|
|
@@ -391,7 +742,29 @@ export default class Workflow extends Command {
|
|
|
391
742
|
prdParser,
|
|
392
743
|
storyTypeDetector,
|
|
393
744
|
});
|
|
394
|
-
this.logger.info({ provider }, 'Services initialized successfully');
|
|
745
|
+
this.logger.info({ provider, verbose }, 'Services initialized successfully');
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Initialize session scaffolder and create session directory structure
|
|
749
|
+
*
|
|
750
|
+
* @param inputPath - Input file path for prefix derivation
|
|
751
|
+
* @param sessionPrefix - Optional explicit session prefix
|
|
752
|
+
* @private
|
|
753
|
+
*/
|
|
754
|
+
async initializeSession(inputPath, sessionPrefix) {
|
|
755
|
+
try {
|
|
756
|
+
// Derive prefix: explicit flag > derived from filename
|
|
757
|
+
const prefix = sessionPrefix || this.derivePrefixFromFilename(inputPath);
|
|
758
|
+
this.sessionDir = await this.scaffolder.createSessionStructure({
|
|
759
|
+
baseDir: 'docs/workflow-sessions',
|
|
760
|
+
prefix,
|
|
761
|
+
});
|
|
762
|
+
this.logger.info({ prefix, sessionDir: this.sessionDir }, 'Workflow session initialized');
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
// Log but don't fail workflow - session is optional (AC: #9)
|
|
766
|
+
this.logger.warn({ error: error.message }, 'Failed to initialize session structure');
|
|
767
|
+
}
|
|
395
768
|
}
|
|
396
769
|
/**
|
|
397
770
|
* Register SIGINT handler for graceful cancellation
|
package/dist/models/index.d.ts
CHANGED
package/dist/models/index.js
CHANGED