@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.
- package/dist/commands/config/show.js +8 -2
- package/dist/commands/decompose.js +26 -5
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/mcp/add.d.ts +16 -0
- package/dist/commands/mcp/add.js +77 -0
- package/dist/commands/mcp/credential/get.d.ts +14 -0
- package/dist/commands/mcp/credential/get.js +35 -0
- package/dist/commands/mcp/credential/list.d.ts +17 -0
- package/dist/commands/mcp/credential/list.js +67 -0
- package/dist/commands/mcp/credential/remove.d.ts +18 -0
- package/dist/commands/mcp/credential/remove.js +84 -0
- package/dist/commands/mcp/credential/set.d.ts +16 -0
- package/dist/commands/mcp/credential/set.js +41 -0
- package/dist/commands/mcp/credential/validate.d.ts +12 -0
- package/dist/commands/mcp/credential/validate.js +150 -0
- package/dist/commands/mcp/list.d.ts +17 -0
- package/dist/commands/mcp/list.js +80 -0
- package/dist/commands/mcp/logs.d.ts +15 -0
- package/dist/commands/mcp/logs.js +64 -0
- package/dist/commands/mcp/preset.d.ts +15 -0
- package/dist/commands/mcp/preset.js +84 -0
- package/dist/commands/mcp/remove.d.ts +14 -0
- package/dist/commands/mcp/remove.js +36 -0
- package/dist/commands/mcp/start.d.ts +12 -0
- package/dist/commands/mcp/start.js +80 -0
- package/dist/commands/mcp/status.d.ts +30 -0
- package/dist/commands/mcp/status.js +180 -0
- package/dist/commands/mcp/stop.d.ts +12 -0
- package/dist/commands/mcp/stop.js +47 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +1 -0
- package/dist/commands/stories/qa.js +34 -75
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +89 -0
- package/dist/commands/workflow.js +487 -14
- package/dist/mcp/types.d.ts +99 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/utils/docker-utils.d.ts +56 -0
- package/dist/mcp/utils/docker-utils.js +108 -0
- package/dist/mcp/utils/template-loader.d.ts +21 -0
- package/dist/mcp/utils/template-loader.js +60 -0
- package/dist/models/agent-options.d.ts +10 -1
- 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/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +25 -4
- package/dist/services/file-system/path-resolver.d.ts +10 -0
- package/dist/services/file-system/path-resolver.js +12 -0
- package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
- package/dist/services/mcp/mcp-config-manager.js +146 -0
- package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
- package/dist/services/mcp/mcp-context-injector.js +168 -0
- package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
- package/dist/services/mcp/mcp-credential-manager.js +124 -0
- package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
- package/dist/services/mcp/mcp-health-checker.js +162 -0
- package/dist/services/mcp/types/health-types.d.ts +31 -0
- package/dist/services/mcp/types/health-types.js +7 -0
- package/dist/services/orchestration/dependency-graph-executor.js +1 -1
- package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
- package/dist/services/orchestration/task-decomposition-service.js +90 -36
- package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
- package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
- package/dist/services/review/ai-review-scanner.d.ts +66 -0
- package/dist/services/review/ai-review-scanner.js +142 -0
- package/dist/services/review/coderabbit-scanner.d.ts +25 -0
- package/dist/services/review/coderabbit-scanner.js +31 -0
- package/dist/services/review/index.d.ts +20 -0
- package/dist/services/review/index.js +15 -0
- package/dist/services/review/lint-scanner.d.ts +46 -0
- package/dist/services/review/lint-scanner.js +172 -0
- package/dist/services/review/review-config.d.ts +62 -0
- package/dist/services/review/review-config.js +91 -0
- package/dist/services/review/review-phase-executor.d.ts +69 -0
- package/dist/services/review/review-phase-executor.js +152 -0
- package/dist/services/review/review-queue.d.ts +98 -0
- package/dist/services/review/review-queue.js +174 -0
- package/dist/services/review/review-reporter.d.ts +94 -0
- package/dist/services/review/review-reporter.js +386 -0
- package/dist/services/review/scanner-factory.d.ts +42 -0
- package/dist/services/review/scanner-factory.js +60 -0
- package/dist/services/review/self-heal-loop.d.ts +58 -0
- package/dist/services/review/self-heal-loop.js +132 -0
- package/dist/services/review/severity-classifier.d.ts +17 -0
- package/dist/services/review/severity-classifier.js +314 -0
- package/dist/services/review/tech-debt-tracker.d.ts +52 -0
- package/dist/services/review/tech-debt-tracker.js +245 -0
- package/dist/services/review/types.d.ts +93 -0
- package/dist/services/review/types.js +23 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- package/dist/utils/credential-utils.d.ts +14 -0
- package/dist/utils/credential-utils.js +19 -0
- package/dist/utils/duration.d.ts +41 -0
- package/dist/utils/duration.js +89 -0
- package/dist/utils/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- 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.
|
|
161
|
+
timeout: Flags.custom({
|
|
162
|
+
parse: async (input) => parseDuration(input),
|
|
163
|
+
})({
|
|
122
164
|
default: 2_700_000,
|
|
123
|
-
description: 'Agent execution timeout
|
|
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: '
|
|
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('
|
|
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
|
|
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
|