@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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* listr2 Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for creating and managing listr2 task structures
|
|
5
|
+
* used by the WorkflowReporter for visualizing workflow progress.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Default terminal width constraint for output formatting
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_TERMINAL_WIDTH = 80;
|
|
11
|
+
/**
|
|
12
|
+
* Regex pattern for detecting ANSI escape codes
|
|
13
|
+
*/
|
|
14
|
+
export declare const ANSI_ESCAPE_PATTERN: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* Strip ANSI escape codes from text
|
|
17
|
+
*
|
|
18
|
+
* @param text - Text potentially containing ANSI codes
|
|
19
|
+
* @returns Clean text without ANSI escape sequences
|
|
20
|
+
*/
|
|
21
|
+
export declare const stripAnsi: (text: string) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Check if the current environment is a TTY
|
|
24
|
+
*
|
|
25
|
+
* @returns true if stdout is a TTY, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
export declare const isTTY: () => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Plain-text status indicators (no ANSI codes)
|
|
30
|
+
*/
|
|
31
|
+
export declare const PLAIN_STATUS_INDICATORS: {
|
|
32
|
+
readonly completed: "[+]";
|
|
33
|
+
readonly failed: "[X]";
|
|
34
|
+
readonly queued: "[.]";
|
|
35
|
+
readonly running: "[*]";
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Format a plain-text phase header for non-TTY output
|
|
39
|
+
*
|
|
40
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
41
|
+
* @returns Plain-text header string (e.g., "=== Phase: Epic Generation ===")
|
|
42
|
+
*/
|
|
43
|
+
export declare const formatPlainPhaseHeader: (phaseName: string) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Format a plain-text spawn marker for non-TTY output
|
|
46
|
+
*
|
|
47
|
+
* @param itemId - Item identifier
|
|
48
|
+
* @param status - Spawn status (STARTED, COMPLETED, FAILED)
|
|
49
|
+
* @param durationMs - Optional duration in milliseconds
|
|
50
|
+
* @param error - Optional error message for failed spawns
|
|
51
|
+
* @returns Plain-text marker string (e.g., "[SPAWN] story-1.001 STARTED")
|
|
52
|
+
*/
|
|
53
|
+
export declare const formatPlainSpawnMarker: (itemId: string, status: "COMPLETED" | "FAILED" | "STARTED", durationMs?: number, error?: string) => string;
|
|
54
|
+
/**
|
|
55
|
+
* Format a plain-text summary for non-TTY output
|
|
56
|
+
*
|
|
57
|
+
* @param totalSpawns - Total number of spawns
|
|
58
|
+
* @param passedCount - Number of passed spawns
|
|
59
|
+
* @param failedCount - Number of failed spawns
|
|
60
|
+
* @param durationMs - Total duration in milliseconds
|
|
61
|
+
* @returns Plain-text summary string
|
|
62
|
+
*/
|
|
63
|
+
export declare const formatPlainSummary: (totalSpawns: number, passedCount: number, failedCount: number, durationMs: number) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Format spawn output with visual delimiters for verbose mode
|
|
66
|
+
*
|
|
67
|
+
* @param spawnId - Spawn identifier
|
|
68
|
+
* @param output - Output content from the spawn
|
|
69
|
+
* @param maxWidth - Maximum line width (default: 80)
|
|
70
|
+
* @returns Formatted output with header/footer delimiters
|
|
71
|
+
*/
|
|
72
|
+
export declare const formatVerboseSpawnOutput: (spawnId: string, output: string, maxWidth?: number) => string;
|
|
73
|
+
/**
|
|
74
|
+
* Wrap text lines to fit within a maximum width
|
|
75
|
+
*
|
|
76
|
+
* @param text - Text to wrap
|
|
77
|
+
* @param maxWidth - Maximum line width
|
|
78
|
+
* @returns Wrapped text
|
|
79
|
+
*/
|
|
80
|
+
export declare const wrapLines: (text: string, maxWidth: number) => string;
|
|
81
|
+
/**
|
|
82
|
+
* Phase emoji mapping for visual consistency
|
|
83
|
+
*/
|
|
84
|
+
export declare const PHASE_EMOJI: Record<string, string>;
|
|
85
|
+
/**
|
|
86
|
+
* Phase display names for titles
|
|
87
|
+
*/
|
|
88
|
+
export declare const PHASE_TITLES: Record<string, string>;
|
|
89
|
+
/**
|
|
90
|
+
* Action verbs for spawn labels — maps phase to what the spawn is DOING (lowercase)
|
|
91
|
+
*/
|
|
92
|
+
export declare const PHASE_ACTION_VERBS: Record<string, string>;
|
|
93
|
+
/**
|
|
94
|
+
* Get the action verb for a phase (e.g., "Creating" for story phase)
|
|
95
|
+
*/
|
|
96
|
+
export declare const getPhaseActionVerb: (phaseName: string) => string;
|
|
97
|
+
/**
|
|
98
|
+
* Status indicators for spawn states
|
|
99
|
+
*/
|
|
100
|
+
export declare const STATUS_INDICATORS: {
|
|
101
|
+
readonly completed: string;
|
|
102
|
+
readonly failed: string;
|
|
103
|
+
readonly queued: string;
|
|
104
|
+
readonly running: string;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Get emoji for a phase name
|
|
108
|
+
*
|
|
109
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
110
|
+
* @returns Emoji string for the phase
|
|
111
|
+
*/
|
|
112
|
+
export declare const getPhaseEmoji: (phaseName: string) => string;
|
|
113
|
+
/**
|
|
114
|
+
* Create a formatted phase title with emoji
|
|
115
|
+
*
|
|
116
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
117
|
+
* @returns Formatted title string with emoji (e.g., "📋 Epic Phase")
|
|
118
|
+
*/
|
|
119
|
+
export declare const createPhaseTitle: (phaseName: string) => string;
|
|
120
|
+
/**
|
|
121
|
+
* Create a phase task group configuration for listr2
|
|
122
|
+
*
|
|
123
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
124
|
+
* @returns Object with title and phase metadata
|
|
125
|
+
*/
|
|
126
|
+
export declare const createPhaseTaskGroup: (phaseName: string) => {
|
|
127
|
+
emoji: string;
|
|
128
|
+
phaseName: string;
|
|
129
|
+
title: string;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Create a layer task group configuration for listr2
|
|
133
|
+
*
|
|
134
|
+
* @param layerIndex - Zero-based layer index
|
|
135
|
+
* @param spawnCount - Number of spawns in the layer
|
|
136
|
+
* @param totalLayers - Total number of layers in the phase
|
|
137
|
+
* @returns Object with title and layer metadata
|
|
138
|
+
*/
|
|
139
|
+
export declare const createLayerTaskGroup: (layerIndex: number, spawnCount: number, totalLayers: number) => {
|
|
140
|
+
layerIndex: number;
|
|
141
|
+
spawnCount: number;
|
|
142
|
+
title: string;
|
|
143
|
+
totalLayers: number;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Spawn status type
|
|
147
|
+
*/
|
|
148
|
+
export type SpawnStatus = 'completed' | 'failed' | 'queued' | 'running';
|
|
149
|
+
/**
|
|
150
|
+
* Format spawn status for display
|
|
151
|
+
*
|
|
152
|
+
* @param status - Current spawn status
|
|
153
|
+
* @param durationMs - Duration in milliseconds (optional)
|
|
154
|
+
* @param itemCount - Number of items processed (optional, for completed status)
|
|
155
|
+
* @param errorMessage - Error message (optional, for failed status)
|
|
156
|
+
* @returns Formatted status string
|
|
157
|
+
*/
|
|
158
|
+
export declare const formatSpawnStatus: (status: SpawnStatus, durationMs?: number, itemCount?: number, errorMessage?: string) => string;
|
|
159
|
+
/**
|
|
160
|
+
* Layer result summary
|
|
161
|
+
*/
|
|
162
|
+
export interface LayerResult {
|
|
163
|
+
durationMs: number;
|
|
164
|
+
failureCount: number;
|
|
165
|
+
layerIndex: number;
|
|
166
|
+
spawnCount: number;
|
|
167
|
+
successCount: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Format a layer summary line for collapsed display
|
|
171
|
+
*
|
|
172
|
+
* @param result - Layer execution results
|
|
173
|
+
* @returns Formatted summary string
|
|
174
|
+
*/
|
|
175
|
+
export declare const formatLayerSummary: (result: LayerResult) => string;
|
|
176
|
+
/**
|
|
177
|
+
* Phase result summary
|
|
178
|
+
*/
|
|
179
|
+
export interface PhaseResult {
|
|
180
|
+
durationMs: number;
|
|
181
|
+
failureCount: number;
|
|
182
|
+
itemCount: number;
|
|
183
|
+
phaseName: string;
|
|
184
|
+
successCount: number;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Format a phase summary for completion display
|
|
188
|
+
*
|
|
189
|
+
* @param result - Phase execution results
|
|
190
|
+
* @returns Formatted summary string
|
|
191
|
+
*/
|
|
192
|
+
export declare const formatPhaseSummary: (result: PhaseResult) => string;
|
|
193
|
+
/**
|
|
194
|
+
* Create a spawn task title
|
|
195
|
+
*
|
|
196
|
+
* @param itemId - Item identifier (epic number, story number, etc.)
|
|
197
|
+
* @param itemTitle - Optional item title/description
|
|
198
|
+
* @param agentType - Type of agent being spawned
|
|
199
|
+
* @returns Formatted spawn task title
|
|
200
|
+
*/
|
|
201
|
+
export declare const createSpawnTitle: (itemId: string, itemTitle?: string, agentType?: string) => string;
|
|
202
|
+
/**
|
|
203
|
+
* Truncate a string to a maximum length with ellipsis
|
|
204
|
+
*
|
|
205
|
+
* @param text - Text to truncate
|
|
206
|
+
* @param maxLength - Maximum length (default: 60)
|
|
207
|
+
* @returns Truncated string with ellipsis if needed
|
|
208
|
+
*/
|
|
209
|
+
export declare const truncateText: (text: string, maxLength?: number) => string;
|
|
210
|
+
/**
|
|
211
|
+
* Format elapsed time for running tasks (updates every second)
|
|
212
|
+
*
|
|
213
|
+
* @param startTime - Start timestamp in milliseconds
|
|
214
|
+
* @returns Formatted elapsed time string
|
|
215
|
+
*/
|
|
216
|
+
export declare const formatElapsedTime: (startTime: number) => string;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* listr2 Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for creating and managing listr2 task structures
|
|
5
|
+
* used by the WorkflowReporter for visualizing workflow progress.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { formatDuration } from './formatters.js';
|
|
9
|
+
/**
|
|
10
|
+
* Default terminal width constraint for output formatting
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_TERMINAL_WIDTH = 80;
|
|
13
|
+
/**
|
|
14
|
+
* Regex pattern for detecting ANSI escape codes
|
|
15
|
+
*/
|
|
16
|
+
export const ANSI_ESCAPE_PATTERN = /\u001B\[[0-9;]*[a-zA-Z]/g;
|
|
17
|
+
/**
|
|
18
|
+
* Strip ANSI escape codes from text
|
|
19
|
+
*
|
|
20
|
+
* @param text - Text potentially containing ANSI codes
|
|
21
|
+
* @returns Clean text without ANSI escape sequences
|
|
22
|
+
*/
|
|
23
|
+
export const stripAnsi = (text) => text.replaceAll(ANSI_ESCAPE_PATTERN, '');
|
|
24
|
+
/**
|
|
25
|
+
* Check if the current environment is a TTY
|
|
26
|
+
*
|
|
27
|
+
* @returns true if stdout is a TTY, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
export const isTTY = () => process.stdout.isTTY === true;
|
|
30
|
+
/**
|
|
31
|
+
* Plain-text status indicators (no ANSI codes)
|
|
32
|
+
*/
|
|
33
|
+
export const PLAIN_STATUS_INDICATORS = {
|
|
34
|
+
completed: '[+]',
|
|
35
|
+
failed: '[X]',
|
|
36
|
+
queued: '[.]',
|
|
37
|
+
running: '[*]',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Format a plain-text phase header for non-TTY output
|
|
41
|
+
*
|
|
42
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
43
|
+
* @returns Plain-text header string (e.g., "=== Phase: Epic Generation ===")
|
|
44
|
+
*/
|
|
45
|
+
export const formatPlainPhaseHeader = (phaseName) => {
|
|
46
|
+
const title = PHASE_TITLES[phaseName.toLowerCase()] || `${phaseName} Phase`;
|
|
47
|
+
return `=== Phase: ${title} ===`;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Format a plain-text spawn marker for non-TTY output
|
|
51
|
+
*
|
|
52
|
+
* @param itemId - Item identifier
|
|
53
|
+
* @param status - Spawn status (STARTED, COMPLETED, FAILED)
|
|
54
|
+
* @param durationMs - Optional duration in milliseconds
|
|
55
|
+
* @param error - Optional error message for failed spawns
|
|
56
|
+
* @returns Plain-text marker string (e.g., "[SPAWN] story-1.001 STARTED")
|
|
57
|
+
*/
|
|
58
|
+
export const formatPlainSpawnMarker = (itemId, status, durationMs, error) => {
|
|
59
|
+
const duration = durationMs === undefined ? '' : ` (${formatDuration(durationMs)})`;
|
|
60
|
+
const errorSuffix = error ? `: ${error}` : '';
|
|
61
|
+
return `[SPAWN] ${itemId} ${status}${duration}${errorSuffix}`;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Format a plain-text summary for non-TTY output
|
|
65
|
+
*
|
|
66
|
+
* @param totalSpawns - Total number of spawns
|
|
67
|
+
* @param passedCount - Number of passed spawns
|
|
68
|
+
* @param failedCount - Number of failed spawns
|
|
69
|
+
* @param durationMs - Total duration in milliseconds
|
|
70
|
+
* @returns Plain-text summary string
|
|
71
|
+
*/
|
|
72
|
+
export const formatPlainSummary = (totalSpawns, passedCount, failedCount, durationMs) => {
|
|
73
|
+
const lines = [
|
|
74
|
+
'--- Summary ---',
|
|
75
|
+
`Total: ${totalSpawns} spawns`,
|
|
76
|
+
`Passed: ${passedCount}`,
|
|
77
|
+
`Failed: ${failedCount}`,
|
|
78
|
+
`Duration: ${formatDuration(durationMs)}`,
|
|
79
|
+
];
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Format spawn output with visual delimiters for verbose mode
|
|
84
|
+
*
|
|
85
|
+
* @param spawnId - Spawn identifier
|
|
86
|
+
* @param output - Output content from the spawn
|
|
87
|
+
* @param maxWidth - Maximum line width (default: 80)
|
|
88
|
+
* @returns Formatted output with header/footer delimiters
|
|
89
|
+
*/
|
|
90
|
+
export const formatVerboseSpawnOutput = (spawnId, output, maxWidth = DEFAULT_TERMINAL_WIDTH) => {
|
|
91
|
+
// Create delimiter line that fits within maxWidth
|
|
92
|
+
const headerText = `── ${spawnId} output ──`;
|
|
93
|
+
const footerText = `── end ${spawnId} ──`;
|
|
94
|
+
// Pad the delimiter to maxWidth with dashes
|
|
95
|
+
const headerPadding = Math.max(0, maxWidth - headerText.length);
|
|
96
|
+
const footerPadding = Math.max(0, maxWidth - footerText.length);
|
|
97
|
+
const header = headerText + '─'.repeat(headerPadding);
|
|
98
|
+
const footer = footerText + '─'.repeat(footerPadding);
|
|
99
|
+
// Wrap output lines to respect width constraint
|
|
100
|
+
const wrappedLines = wrapLines(output, maxWidth);
|
|
101
|
+
return `${header}\n${wrappedLines}\n${footer}`;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Wrap text lines to fit within a maximum width
|
|
105
|
+
*
|
|
106
|
+
* @param text - Text to wrap
|
|
107
|
+
* @param maxWidth - Maximum line width
|
|
108
|
+
* @returns Wrapped text
|
|
109
|
+
*/
|
|
110
|
+
export const wrapLines = (text, maxWidth) => {
|
|
111
|
+
const lines = text.split('\n');
|
|
112
|
+
const wrappedLines = [];
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
if (line.length <= maxWidth) {
|
|
115
|
+
wrappedLines.push(line);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Simple word-based wrapping
|
|
119
|
+
let currentLine = '';
|
|
120
|
+
const words = line.split(' ');
|
|
121
|
+
for (const word of words) {
|
|
122
|
+
if (currentLine.length === 0) {
|
|
123
|
+
currentLine = word;
|
|
124
|
+
}
|
|
125
|
+
else if (currentLine.length + 1 + word.length <= maxWidth) {
|
|
126
|
+
currentLine += ' ' + word;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
wrappedLines.push(currentLine);
|
|
130
|
+
currentLine = word;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (currentLine.length > 0) {
|
|
134
|
+
wrappedLines.push(currentLine);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return wrappedLines.join('\n');
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Phase emoji mapping for visual consistency
|
|
142
|
+
*/
|
|
143
|
+
export const PHASE_EMOJI = {
|
|
144
|
+
dev: '🛠️',
|
|
145
|
+
epic: '📋',
|
|
146
|
+
qa: '✅',
|
|
147
|
+
story: '📝',
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Phase display names for titles
|
|
151
|
+
*/
|
|
152
|
+
export const PHASE_TITLES = {
|
|
153
|
+
dev: 'Dev',
|
|
154
|
+
epic: 'Epics',
|
|
155
|
+
qa: 'QA',
|
|
156
|
+
story: 'Stories',
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Action verbs for spawn labels — maps phase to what the spawn is DOING (lowercase)
|
|
160
|
+
*/
|
|
161
|
+
export const PHASE_ACTION_VERBS = {
|
|
162
|
+
dev: 'developing',
|
|
163
|
+
epic: 'generating',
|
|
164
|
+
qa: 'reviewing',
|
|
165
|
+
story: 'creating',
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Get the action verb for a phase (e.g., "Creating" for story phase)
|
|
169
|
+
*/
|
|
170
|
+
export const getPhaseActionVerb = (phaseName) => PHASE_ACTION_VERBS[phaseName.toLowerCase()] || 'Processing';
|
|
171
|
+
/**
|
|
172
|
+
* Status indicators for spawn states
|
|
173
|
+
*/
|
|
174
|
+
export const STATUS_INDICATORS = {
|
|
175
|
+
completed: chalk.bold('✓'),
|
|
176
|
+
failed: chalk.bold('✗'),
|
|
177
|
+
queued: chalk.dim('◯'),
|
|
178
|
+
running: chalk.bold('◉'),
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Get emoji for a phase name
|
|
182
|
+
*
|
|
183
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
184
|
+
* @returns Emoji string for the phase
|
|
185
|
+
*/
|
|
186
|
+
export const getPhaseEmoji = (phaseName) => PHASE_EMOJI[phaseName.toLowerCase()] || '⚙️';
|
|
187
|
+
/**
|
|
188
|
+
* Create a formatted phase title with emoji
|
|
189
|
+
*
|
|
190
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
191
|
+
* @returns Formatted title string with emoji (e.g., "📋 Epic Phase")
|
|
192
|
+
*/
|
|
193
|
+
export const createPhaseTitle = (phaseName) => {
|
|
194
|
+
const emoji = getPhaseEmoji(phaseName);
|
|
195
|
+
const title = PHASE_TITLES[phaseName.toLowerCase()] || `${phaseName} Phase`;
|
|
196
|
+
return `${emoji} ${title}`;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Create a phase task group configuration for listr2
|
|
200
|
+
*
|
|
201
|
+
* @param phaseName - Phase name (epic, story, dev, qa)
|
|
202
|
+
* @returns Object with title and phase metadata
|
|
203
|
+
*/
|
|
204
|
+
export const createPhaseTaskGroup = (phaseName) => ({
|
|
205
|
+
emoji: getPhaseEmoji(phaseName),
|
|
206
|
+
phaseName: phaseName.toLowerCase(),
|
|
207
|
+
title: createPhaseTitle(phaseName),
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* Create a layer task group configuration for listr2
|
|
211
|
+
*
|
|
212
|
+
* @param layerIndex - Zero-based layer index
|
|
213
|
+
* @param spawnCount - Number of spawns in the layer
|
|
214
|
+
* @param totalLayers - Total number of layers in the phase
|
|
215
|
+
* @returns Object with title and layer metadata
|
|
216
|
+
*/
|
|
217
|
+
export const createLayerTaskGroup = (layerIndex, spawnCount, totalLayers) => {
|
|
218
|
+
const layerNumber = layerIndex + 1;
|
|
219
|
+
const title = `Layer ${layerNumber}/${totalLayers} (${spawnCount} ${spawnCount === 1 ? 'spawn' : 'spawns'})`;
|
|
220
|
+
return {
|
|
221
|
+
layerIndex,
|
|
222
|
+
spawnCount,
|
|
223
|
+
title,
|
|
224
|
+
totalLayers,
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Format spawn status for display
|
|
229
|
+
*
|
|
230
|
+
* @param status - Current spawn status
|
|
231
|
+
* @param durationMs - Duration in milliseconds (optional)
|
|
232
|
+
* @param itemCount - Number of items processed (optional, for completed status)
|
|
233
|
+
* @param errorMessage - Error message (optional, for failed status)
|
|
234
|
+
* @returns Formatted status string
|
|
235
|
+
*/
|
|
236
|
+
export const formatSpawnStatus = (status, durationMs, itemCount, errorMessage) => {
|
|
237
|
+
const indicator = STATUS_INDICATORS[status];
|
|
238
|
+
const duration = durationMs === undefined ? '' : formatDuration(durationMs);
|
|
239
|
+
switch (status) {
|
|
240
|
+
case 'completed': {
|
|
241
|
+
if (itemCount !== undefined) {
|
|
242
|
+
return duration
|
|
243
|
+
? `${indicator} completed (${duration}) → ${itemCount} ${itemCount === 1 ? 'item' : 'items'}`
|
|
244
|
+
: `${indicator} completed → ${itemCount} ${itemCount === 1 ? 'item' : 'items'}`;
|
|
245
|
+
}
|
|
246
|
+
return duration ? `${indicator} completed (${duration})` : `${indicator} completed`;
|
|
247
|
+
}
|
|
248
|
+
case 'failed': {
|
|
249
|
+
return errorMessage
|
|
250
|
+
? `${indicator} failed: ${errorMessage}`
|
|
251
|
+
: `${indicator} failed`;
|
|
252
|
+
}
|
|
253
|
+
case 'queued': {
|
|
254
|
+
return `${indicator} queued`;
|
|
255
|
+
}
|
|
256
|
+
case 'running': {
|
|
257
|
+
return duration ? `${indicator} running (${duration})` : `${indicator} running`;
|
|
258
|
+
}
|
|
259
|
+
default: {
|
|
260
|
+
return `${indicator} ${status}`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Format a layer summary line for collapsed display
|
|
266
|
+
*
|
|
267
|
+
* @param result - Layer execution results
|
|
268
|
+
* @returns Formatted summary string
|
|
269
|
+
*/
|
|
270
|
+
export const formatLayerSummary = (result) => {
|
|
271
|
+
const layerNumber = result.layerIndex + 1;
|
|
272
|
+
const duration = formatDuration(result.durationMs);
|
|
273
|
+
const passedText = `${result.successCount}/${result.spawnCount} passed`;
|
|
274
|
+
const statusColor = result.failureCount > 0
|
|
275
|
+
? chalk.bold
|
|
276
|
+
: result.successCount === result.spawnCount
|
|
277
|
+
? chalk.dim
|
|
278
|
+
: chalk.underline;
|
|
279
|
+
return `Layer ${layerNumber} (${result.spawnCount} ${result.spawnCount === 1 ? 'spawn' : 'spawns'}) — completed in ${duration} [${statusColor(passedText)}]`;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Format a phase summary for completion display
|
|
283
|
+
*
|
|
284
|
+
* @param result - Phase execution results
|
|
285
|
+
* @returns Formatted summary string
|
|
286
|
+
*/
|
|
287
|
+
export const formatPhaseSummary = (result) => {
|
|
288
|
+
const emoji = getPhaseEmoji(result.phaseName);
|
|
289
|
+
const title = PHASE_TITLES[result.phaseName.toLowerCase()] || `${result.phaseName} Phase`;
|
|
290
|
+
const duration = formatDuration(result.durationMs);
|
|
291
|
+
const passedText = `${result.successCount}/${result.itemCount}`;
|
|
292
|
+
const statusIcon = result.failureCount > 0
|
|
293
|
+
? chalk.bold('✗')
|
|
294
|
+
: result.successCount === result.itemCount
|
|
295
|
+
? chalk.bold('✓')
|
|
296
|
+
: chalk.bold('⚠');
|
|
297
|
+
return `${emoji} ${title} ${statusIcon} — ${passedText} in ${duration}`;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Create a spawn task title
|
|
301
|
+
*
|
|
302
|
+
* @param itemId - Item identifier (epic number, story number, etc.)
|
|
303
|
+
* @param itemTitle - Optional item title/description
|
|
304
|
+
* @param agentType - Type of agent being spawned
|
|
305
|
+
* @returns Formatted spawn task title
|
|
306
|
+
*/
|
|
307
|
+
export const createSpawnTitle = (itemId, itemTitle, agentType) => {
|
|
308
|
+
const prefix = agentType ? chalk.dim(`[${agentType}]`) : '';
|
|
309
|
+
const title = itemTitle ? `${itemId}: ${itemTitle}` : itemId;
|
|
310
|
+
return prefix ? `${prefix} ${title}` : title;
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Truncate a string to a maximum length with ellipsis
|
|
314
|
+
*
|
|
315
|
+
* @param text - Text to truncate
|
|
316
|
+
* @param maxLength - Maximum length (default: 60)
|
|
317
|
+
* @returns Truncated string with ellipsis if needed
|
|
318
|
+
*/
|
|
319
|
+
export const truncateText = (text, maxLength = 60) => {
|
|
320
|
+
if (text.length <= maxLength) {
|
|
321
|
+
return text;
|
|
322
|
+
}
|
|
323
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Format elapsed time for running tasks (updates every second)
|
|
327
|
+
*
|
|
328
|
+
* @param startTime - Start timestamp in milliseconds
|
|
329
|
+
* @returns Formatted elapsed time string
|
|
330
|
+
*/
|
|
331
|
+
export const formatElapsedTime = (startTime) => {
|
|
332
|
+
const elapsed = Date.now() - startTime;
|
|
333
|
+
return formatDuration(elapsed);
|
|
334
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperdrive.bot/bmad-workflow",
|
|
3
3
|
"description": "AI-driven development workflow orchestration CLI for BMAD projects",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.18",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DevSquad",
|
|
7
7
|
"email": "marcelo@devsquad.email",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"fs-extra": "^11.3.2",
|
|
24
24
|
"js-yaml": "^4.1.0",
|
|
25
25
|
"jsonwebtoken": "^9.0.2",
|
|
26
|
+
"listr2": "^10.1.0",
|
|
26
27
|
"nodemailer": "^7.0.9",
|
|
27
28
|
"ora": "^9.0.0",
|
|
28
29
|
"pino": "^10.0.0",
|
|
@@ -52,10 +53,10 @@
|
|
|
52
53
|
"eslint": "^9",
|
|
53
54
|
"eslint-config-oclif": "^6",
|
|
54
55
|
"eslint-config-prettier": "^10",
|
|
56
|
+
"esmock": "^2.7.3",
|
|
55
57
|
"mocha": "^10.8.2",
|
|
56
58
|
"oclif": "^4",
|
|
57
59
|
"prettier": "^3.6.2",
|
|
58
|
-
"esmock": "^2.7.3",
|
|
59
60
|
"proxyquire": "^2.1.3",
|
|
60
61
|
"sinon": "^17.0.1",
|
|
61
62
|
"ts-node": "^10.9.2",
|