@hyperdrive.bot/bmad-workflow 1.0.2

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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1017 -0
  3. package/bin/dev +5 -0
  4. package/bin/dev.cmd +3 -0
  5. package/bin/dev.js +5 -0
  6. package/bin/run +5 -0
  7. package/bin/run.cmd +3 -0
  8. package/bin/run.js +5 -0
  9. package/dist/commands/config/show.d.ts +34 -0
  10. package/dist/commands/config/show.js +108 -0
  11. package/dist/commands/config/validate.d.ts +29 -0
  12. package/dist/commands/config/validate.js +131 -0
  13. package/dist/commands/decompose.d.ts +79 -0
  14. package/dist/commands/decompose.js +327 -0
  15. package/dist/commands/demo.d.ts +18 -0
  16. package/dist/commands/demo.js +107 -0
  17. package/dist/commands/epics/create.d.ts +123 -0
  18. package/dist/commands/epics/create.js +459 -0
  19. package/dist/commands/epics/list.d.ts +120 -0
  20. package/dist/commands/epics/list.js +280 -0
  21. package/dist/commands/hello/index.d.ts +12 -0
  22. package/dist/commands/hello/index.js +34 -0
  23. package/dist/commands/hello/world.d.ts +8 -0
  24. package/dist/commands/hello/world.js +24 -0
  25. package/dist/commands/prd/fix.d.ts +39 -0
  26. package/dist/commands/prd/fix.js +140 -0
  27. package/dist/commands/prd/validate.d.ts +112 -0
  28. package/dist/commands/prd/validate.js +302 -0
  29. package/dist/commands/stories/create.d.ts +95 -0
  30. package/dist/commands/stories/create.js +431 -0
  31. package/dist/commands/stories/develop.d.ts +91 -0
  32. package/dist/commands/stories/develop.js +460 -0
  33. package/dist/commands/stories/list.d.ts +84 -0
  34. package/dist/commands/stories/list.js +291 -0
  35. package/dist/commands/stories/move.d.ts +66 -0
  36. package/dist/commands/stories/move.js +273 -0
  37. package/dist/commands/stories/qa.d.ts +99 -0
  38. package/dist/commands/stories/qa.js +530 -0
  39. package/dist/commands/workflow.d.ts +97 -0
  40. package/dist/commands/workflow.js +390 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +1 -0
  43. package/dist/models/agent-options.d.ts +50 -0
  44. package/dist/models/agent-options.js +1 -0
  45. package/dist/models/agent-result.d.ts +29 -0
  46. package/dist/models/agent-result.js +1 -0
  47. package/dist/models/index.d.ts +10 -0
  48. package/dist/models/index.js +10 -0
  49. package/dist/models/phase-result.d.ts +65 -0
  50. package/dist/models/phase-result.js +7 -0
  51. package/dist/models/provider.d.ts +28 -0
  52. package/dist/models/provider.js +18 -0
  53. package/dist/models/story.d.ts +154 -0
  54. package/dist/models/story.js +18 -0
  55. package/dist/models/workflow-config.d.ts +148 -0
  56. package/dist/models/workflow-config.js +1 -0
  57. package/dist/models/workflow-result.d.ts +164 -0
  58. package/dist/models/workflow-result.js +7 -0
  59. package/dist/services/agents/agent-runner-factory.d.ts +31 -0
  60. package/dist/services/agents/agent-runner-factory.js +44 -0
  61. package/dist/services/agents/agent-runner.d.ts +46 -0
  62. package/dist/services/agents/agent-runner.js +29 -0
  63. package/dist/services/agents/claude-agent-runner.d.ts +81 -0
  64. package/dist/services/agents/claude-agent-runner.js +332 -0
  65. package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
  66. package/dist/services/agents/gemini-agent-runner.js +350 -0
  67. package/dist/services/agents/index.d.ts +7 -0
  68. package/dist/services/agents/index.js +7 -0
  69. package/dist/services/file-system/file-manager.d.ts +110 -0
  70. package/dist/services/file-system/file-manager.js +223 -0
  71. package/dist/services/file-system/glob-matcher.d.ts +75 -0
  72. package/dist/services/file-system/glob-matcher.js +126 -0
  73. package/dist/services/file-system/path-resolver.d.ts +183 -0
  74. package/dist/services/file-system/path-resolver.js +400 -0
  75. package/dist/services/logging/workflow-logger.d.ts +232 -0
  76. package/dist/services/logging/workflow-logger.js +552 -0
  77. package/dist/services/orchestration/batch-processor.d.ts +113 -0
  78. package/dist/services/orchestration/batch-processor.js +187 -0
  79. package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
  80. package/dist/services/orchestration/dependency-graph-executor.js +447 -0
  81. package/dist/services/orchestration/index.d.ts +10 -0
  82. package/dist/services/orchestration/index.js +8 -0
  83. package/dist/services/orchestration/input-detector.d.ts +125 -0
  84. package/dist/services/orchestration/input-detector.js +381 -0
  85. package/dist/services/orchestration/story-queue.d.ts +94 -0
  86. package/dist/services/orchestration/story-queue.js +170 -0
  87. package/dist/services/orchestration/story-type-detector.d.ts +80 -0
  88. package/dist/services/orchestration/story-type-detector.js +258 -0
  89. package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
  90. package/dist/services/orchestration/task-decomposition-service.js +607 -0
  91. package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
  92. package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
  93. package/dist/services/parsers/epic-parser.d.ts +117 -0
  94. package/dist/services/parsers/epic-parser.js +264 -0
  95. package/dist/services/parsers/prd-fixer.d.ts +86 -0
  96. package/dist/services/parsers/prd-fixer.js +194 -0
  97. package/dist/services/parsers/prd-parser.d.ts +123 -0
  98. package/dist/services/parsers/prd-parser.js +286 -0
  99. package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
  100. package/dist/services/parsers/standalone-story-parser.js +255 -0
  101. package/dist/services/parsers/story-parser-factory.d.ts +81 -0
  102. package/dist/services/parsers/story-parser-factory.js +108 -0
  103. package/dist/services/parsers/story-parser.d.ts +122 -0
  104. package/dist/services/parsers/story-parser.js +262 -0
  105. package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
  106. package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
  107. package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
  108. package/dist/services/scaffolding/file-scaffolder.js +314 -0
  109. package/dist/services/validation/config-validator.d.ts +88 -0
  110. package/dist/services/validation/config-validator.js +167 -0
  111. package/dist/types/task-graph.d.ts +142 -0
  112. package/dist/types/task-graph.js +5 -0
  113. package/dist/utils/colors.d.ts +49 -0
  114. package/dist/utils/colors.js +50 -0
  115. package/dist/utils/error-formatter.d.ts +64 -0
  116. package/dist/utils/error-formatter.js +279 -0
  117. package/dist/utils/errors.d.ts +170 -0
  118. package/dist/utils/errors.js +233 -0
  119. package/dist/utils/formatters.d.ts +84 -0
  120. package/dist/utils/formatters.js +162 -0
  121. package/dist/utils/logger.d.ts +63 -0
  122. package/dist/utils/logger.js +78 -0
  123. package/dist/utils/progress.d.ts +104 -0
  124. package/dist/utils/progress.js +161 -0
  125. package/dist/utils/retry.d.ts +114 -0
  126. package/dist/utils/retry.js +160 -0
  127. package/dist/utils/shared-flags.d.ts +28 -0
  128. package/dist/utils/shared-flags.js +43 -0
  129. package/package.json +119 -0
@@ -0,0 +1,552 @@
1
+ /**
2
+ * WorkflowLogger Service
3
+ *
4
+ * Comprehensive logging for workflow executions including:
5
+ * - Execution metadata and configuration
6
+ * - Prompts sent to Claude agents
7
+ * - Responses received from agents
8
+ * - Execution timeline and state
9
+ * - Success/failure tracking
10
+ */
11
+ import fs from 'fs-extra';
12
+ import path from 'node:path';
13
+ export class WorkflowLogger {
14
+ logger;
15
+ logsDir;
16
+ logFile;
17
+ logFilePath;
18
+ workflowId;
19
+ constructor(logger, logsDir = '.bmad-core/logs') {
20
+ this.logger = logger;
21
+ this.logsDir = logsDir;
22
+ this.workflowId = this.generateWorkflowId();
23
+ this.logFilePath = path.join(this.logsDir, `workflow-${this.workflowId}.json`);
24
+ this.logFile = {
25
+ configuration: {},
26
+ errors: [],
27
+ executionPlan: {
28
+ epicsExisting: 0,
29
+ epicsToCreate: 0,
30
+ estimatedDuration: '0m',
31
+ storiesExisting: 0,
32
+ storiesToCreate: 0,
33
+ storiesToDevelop: 0,
34
+ },
35
+ metadata: {
36
+ startTime: new Date().toISOString(),
37
+ status: 'running',
38
+ workflowId: this.workflowId,
39
+ },
40
+ prompts: [],
41
+ responses: [],
42
+ summary: {
43
+ completed: { developments: 0, epics: 0, stories: 0 },
44
+ failed: { developments: 0, epics: 0, stories: 0 },
45
+ pending: { developments: 0, epics: 0, stories: 0 },
46
+ },
47
+ timeline: [],
48
+ };
49
+ }
50
+ /**
51
+ * Generate pipeline summary table for concurrent phases
52
+ *
53
+ * Creates a formatted table showing the status of each phase in the pipeline,
54
+ * including pending, in progress, completed, and failed counts.
55
+ *
56
+ * @returns Formatted summary table string
57
+ */
58
+ generatePipelineSummaryTable() {
59
+ if (!this.logFile.pipeline) {
60
+ return '';
61
+ }
62
+ const { pipeline, summary } = this.logFile;
63
+ const table = [];
64
+ table.push('╔═══════════════════════╦═══════════╦═════════════╦═══════════╦═════════╗', '║ Phase ║ Pending ║ In Progress ║ Completed ║ Failed ║', '╠═══════════════════════╬═══════════╬═════════════╬═══════════╬═════════╣');
65
+ // Story Creation row
66
+ const storyCreationPending = 0; // Not tracked separately
67
+ const storyCreationInProgress = pipeline.storiesCreating;
68
+ const storyCreationCompleted = summary.completed.stories;
69
+ const storyCreationFailed = summary.failed.stories;
70
+ table.push(`║ Story Creation ║ ${this.padNumber(storyCreationPending, 9)} ║ ${this.padNumber(storyCreationInProgress, 11)} ║ ${this.padNumber(storyCreationCompleted, 9)} ║ ${this.padNumber(storyCreationFailed, 7)} ║`);
71
+ // Story Queue row
72
+ const storyQueuePending = 0; // Not tracked separately
73
+ const storyQueueInProgress = pipeline.storiesQueued;
74
+ const storyQueueCompleted = 0; // Stories leave queue when picked up
75
+ const storyQueueFailed = 0; // Queue doesn't track failures
76
+ table.push(`║ Story Queue ║ ${this.padNumber(storyQueuePending, 9)} ║ ${this.padNumber(storyQueueInProgress, 11)} ║ ${this.padNumber(storyQueueCompleted, 9)} ║ ${this.padNumber(storyQueueFailed, 7)} ║`);
77
+ // Development row (with worker count)
78
+ const devPending = 0; // Not tracked separately
79
+ const devInProgress = pipeline.storiesDeveloping;
80
+ const devCompleted = summary.completed.developments;
81
+ const devFailed = summary.failed.developments;
82
+ table.push(`║ Development (${pipeline.activeWorkers}/${pipeline.activeWorkers} workers) ║ ${this.padNumber(devPending, 9)} ║ ${this.padNumber(devInProgress, 11)} ║ ${this.padNumber(devCompleted, 9)} ║ ${this.padNumber(devFailed, 7)} ║`);
83
+ // Overall row
84
+ const overallPending = storyCreationPending + storyQueuePending + devPending;
85
+ const overallInProgress = storyCreationInProgress + storyQueueInProgress + devInProgress;
86
+ const overallCompleted = pipeline.storiesCompleted;
87
+ const overallFailed = storyCreationFailed + storyQueueFailed + devFailed;
88
+ table.push('╠═══════════════════════╬═══════════╬═════════════╬═══════════╬═════════╣', `║ Overall ║ ${this.padNumber(overallPending, 9)} ║ ${this.padNumber(overallInProgress, 11)} ║ ${this.padNumber(overallCompleted, 9)} ║ ${this.padNumber(overallFailed, 7)} ║`, '╚═══════════════════════╩═══════════╩═════════════╩═══════════╩═════════╝');
89
+ return table.join('\n');
90
+ }
91
+ /**
92
+ * Get log file path
93
+ */
94
+ getLogFilePath() {
95
+ return this.logFilePath;
96
+ }
97
+ /**
98
+ * Get current pipeline state
99
+ */
100
+ getPipelineState() {
101
+ return this.logFile.pipeline ? { ...this.logFile.pipeline } : undefined;
102
+ }
103
+ /**
104
+ * Get workflow ID
105
+ */
106
+ getWorkflowId() {
107
+ return this.workflowId;
108
+ }
109
+ /**
110
+ * Initialize workflow log with configuration
111
+ */
112
+ async initialize(config) {
113
+ this.logFile.configuration = config;
114
+ // Ensure logs directory exists
115
+ await fs.ensureDir(this.logsDir);
116
+ this.addLogEntry('workflow', 'initialization', {
117
+ config,
118
+ logFile: this.logFilePath,
119
+ workflowId: this.workflowId,
120
+ });
121
+ await this.writeLogFile();
122
+ this.logger.info({
123
+ logFile: this.logFilePath,
124
+ workflowId: this.workflowId,
125
+ }, 'Workflow log initialized');
126
+ }
127
+ /**
128
+ * Initialize pipeline state tracking
129
+ */
130
+ initializePipelineState() {
131
+ this.logFile.pipeline = {
132
+ activeWorkers: 0,
133
+ storiesCompleted: 0,
134
+ storiesCreating: 0,
135
+ storiesDeveloping: 0,
136
+ storiesQueued: 0,
137
+ };
138
+ }
139
+ /**
140
+ * Log phase completion
141
+ */
142
+ async logPhaseComplete(phase, successCount, failureCount, duration) {
143
+ // Update summary
144
+ switch (phase) {
145
+ case 'dev': {
146
+ this.logFile.summary.completed.developments = successCount;
147
+ this.logFile.summary.failed.developments = failureCount;
148
+ break;
149
+ }
150
+ case 'epic': {
151
+ this.logFile.summary.completed.epics = successCount;
152
+ this.logFile.summary.failed.epics = failureCount;
153
+ break;
154
+ }
155
+ case 'story': {
156
+ this.logFile.summary.completed.stories = successCount;
157
+ this.logFile.summary.failed.stories = failureCount;
158
+ break;
159
+ }
160
+ // No default
161
+ }
162
+ const logDetails = {
163
+ duration,
164
+ failed: failureCount,
165
+ success: successCount,
166
+ };
167
+ // Include pipeline state if available
168
+ if (this.logFile.pipeline) {
169
+ logDetails.pipelineState = { ...this.logFile.pipeline };
170
+ }
171
+ this.addLogEntry(phase, 'phase-complete', logDetails);
172
+ await this.writeLogFile();
173
+ }
174
+ /**
175
+ * Log phase start
176
+ */
177
+ async logPhaseStart(phase, details) {
178
+ this.addLogEntry(phase, 'phase-start', details);
179
+ await this.writeLogFile();
180
+ }
181
+ /**
182
+ * Log a prompt being sent to Claude
183
+ */
184
+ async logPrompt(phase, itemIdentifier, prompt, options) {
185
+ const entry = {
186
+ agentType: options.agentType,
187
+ itemIdentifier,
188
+ phase,
189
+ prompt,
190
+ promptLength: prompt.length,
191
+ references: options.references || [],
192
+ timeout: options.timeout || 1_800_000,
193
+ timestamp: new Date().toISOString(),
194
+ };
195
+ this.logFile.prompts.push(entry);
196
+ this.addLogEntry(phase, 'prompt-sent', {
197
+ agentType: options.agentType,
198
+ identifier: itemIdentifier,
199
+ promptLength: prompt.length,
200
+ });
201
+ await this.writeLogFile();
202
+ }
203
+ /**
204
+ * Log a response received from Claude
205
+ */
206
+ async logResponse(phase, itemIdentifier, result) {
207
+ const entry = {
208
+ agentType: result.agentType,
209
+ duration: result.duration,
210
+ errors: result.errors,
211
+ exitCode: result.exitCode,
212
+ itemIdentifier,
213
+ output: result.output,
214
+ outputLength: result.output.length,
215
+ phase,
216
+ success: result.success,
217
+ timestamp: new Date().toISOString(),
218
+ };
219
+ this.logFile.responses.push(entry);
220
+ this.addLogEntry(phase, result.success ? 'response-success' : 'response-failure', {
221
+ agentType: result.agentType,
222
+ duration: result.duration,
223
+ exitCode: result.exitCode,
224
+ identifier: itemIdentifier,
225
+ outputLength: result.output.length,
226
+ success: result.success,
227
+ });
228
+ if (!result.success) {
229
+ this.logError(phase, itemIdentifier, result.errors);
230
+ }
231
+ await this.writeLogFile();
232
+ }
233
+ /**
234
+ * Log story development completed in pipeline mode
235
+ */
236
+ async logStoryCompleted(storyIdentifier, duration, success) {
237
+ if (!this.logFile.pipeline) {
238
+ this.initializePipelineState();
239
+ }
240
+ this.logFile.pipeline.storiesCompleted++;
241
+ if (this.logFile.pipeline.storiesDeveloping > 0) {
242
+ this.logFile.pipeline.storiesDeveloping--;
243
+ }
244
+ this.addLogEntry('dev', success ? 'story-completed' : 'story-failed', {
245
+ duration,
246
+ identifier: storyIdentifier,
247
+ pipelineState: { ...this.logFile.pipeline },
248
+ success,
249
+ });
250
+ await this.writeLogFile();
251
+ }
252
+ /**
253
+ * Log story created in pipeline mode
254
+ */
255
+ async logStoryCreated(storyIdentifier) {
256
+ if (!this.logFile.pipeline) {
257
+ this.initializePipelineState();
258
+ }
259
+ this.logFile.pipeline.storiesCreating++;
260
+ this.addLogEntry('story', 'story-created', {
261
+ identifier: storyIdentifier,
262
+ pipelineState: { ...this.logFile.pipeline },
263
+ });
264
+ await this.writeLogFile();
265
+ }
266
+ /**
267
+ * Log story development started in pipeline mode
268
+ */
269
+ async logStoryDeveloping(storyIdentifier, workerId) {
270
+ if (!this.logFile.pipeline) {
271
+ this.initializePipelineState();
272
+ }
273
+ this.logFile.pipeline.storiesDeveloping++;
274
+ if (this.logFile.pipeline.storiesQueued > 0) {
275
+ this.logFile.pipeline.storiesQueued--;
276
+ }
277
+ this.addLogEntry('dev', 'story-developing', {
278
+ identifier: storyIdentifier,
279
+ pipelineState: { ...this.logFile.pipeline },
280
+ workerId,
281
+ });
282
+ await this.writeLogFile();
283
+ }
284
+ /**
285
+ * Log story queued for development in pipeline mode
286
+ */
287
+ async logStoryQueued(storyIdentifier, queuePosition) {
288
+ if (!this.logFile.pipeline) {
289
+ this.initializePipelineState();
290
+ }
291
+ this.logFile.pipeline.storiesQueued++;
292
+ if (this.logFile.pipeline.storiesCreating > 0) {
293
+ this.logFile.pipeline.storiesCreating--;
294
+ }
295
+ this.addLogEntry('story', 'story-queued', {
296
+ identifier: storyIdentifier,
297
+ pipelineState: { ...this.logFile.pipeline },
298
+ queuePosition,
299
+ });
300
+ await this.writeLogFile();
301
+ }
302
+ /**
303
+ * Log verbose story transition with timestamp
304
+ *
305
+ * Formats transition as: [HH:MM:SS] Story X.Y 'Title' → STATE
306
+ *
307
+ * @param storyNumber - Story number (e.g., "1.1")
308
+ * @param storyTitle - Story title
309
+ * @param state - New state (CREATING, QUEUED, DEVELOPING, COMPLETED, FAILED)
310
+ * @param details - Optional additional details
311
+ */
312
+ async logVerboseTransition(storyNumber, storyTitle, state, details) {
313
+ const timestamp = this.formatTime(new Date());
314
+ const baseMessage = `[${timestamp}] Story ${storyNumber} '${storyTitle}' → ${state}`;
315
+ let fullMessage = baseMessage;
316
+ if (details) {
317
+ if (details.position !== undefined) {
318
+ fullMessage += ` (position: ${details.position})`;
319
+ }
320
+ if (details.workerId !== undefined) {
321
+ fullMessage += ` (worker: ${details.workerId})`;
322
+ }
323
+ if (details.duration !== undefined) {
324
+ fullMessage += ` (duration: ${this.formatDuration(details.duration)})`;
325
+ }
326
+ }
327
+ this.logger.debug(fullMessage);
328
+ // Also add to timeline for permanent record
329
+ this.addLogEntry('story', 'verbose-transition', {
330
+ state,
331
+ storyNumber,
332
+ storyTitle,
333
+ timestamp: new Date().toISOString(),
334
+ ...details,
335
+ });
336
+ await this.writeLogFile();
337
+ }
338
+ /**
339
+ * Log workflow completion
340
+ */
341
+ async logWorkflowComplete(result) {
342
+ const endTime = new Date().toISOString();
343
+ const startTime = new Date(this.logFile.metadata.startTime);
344
+ const duration = Date.now() - startTime.getTime();
345
+ this.logFile.metadata.endTime = endTime;
346
+ this.logFile.metadata.duration = duration;
347
+ this.logFile.metadata.status = result.overallSuccess ? 'completed' : 'failed';
348
+ this.addLogEntry('workflow', 'workflow-complete', {
349
+ duration,
350
+ overallSuccess: result.overallSuccess,
351
+ totalFailures: result.totalFailures,
352
+ totalFilesProcessed: result.totalFilesProcessed,
353
+ });
354
+ await this.writeLogFile();
355
+ // Generate human-readable summary
356
+ await this.generateMarkdownSummary();
357
+ }
358
+ /**
359
+ * Set execution plan
360
+ */
361
+ async setExecutionPlan(plan) {
362
+ this.logFile.executionPlan = plan;
363
+ this.addLogEntry('workflow', 'execution-plan', {
364
+ plan,
365
+ });
366
+ await this.writeLogFile();
367
+ }
368
+ /**
369
+ * Update active worker count in pipeline mode
370
+ */
371
+ async updateActiveWorkers(count) {
372
+ if (!this.logFile.pipeline) {
373
+ this.initializePipelineState();
374
+ }
375
+ this.logFile.pipeline.activeWorkers = count;
376
+ this.addLogEntry('dev', 'workers-updated', {
377
+ activeWorkers: count,
378
+ pipelineState: { ...this.logFile.pipeline },
379
+ });
380
+ await this.writeLogFile();
381
+ }
382
+ /**
383
+ * Add a timeline entry
384
+ */
385
+ addLogEntry(phase, action, details) {
386
+ this.logFile.timeline.push({
387
+ action,
388
+ details,
389
+ level: 'info',
390
+ phase,
391
+ timestamp: new Date().toISOString(),
392
+ });
393
+ }
394
+ /**
395
+ * Build markdown summary content
396
+ */
397
+ buildMarkdownSummary() {
398
+ const { configuration, errors, executionPlan, metadata, prompts, responses, summary } = this.logFile;
399
+ let md = `# Workflow Execution Log\n\n`;
400
+ // Metadata
401
+ md += `## Metadata\n\n`;
402
+ md += `- **Workflow ID:** ${metadata.workflowId}\n`;
403
+ md += `- **Start Time:** ${metadata.startTime}\n`;
404
+ md += `- **End Time:** ${metadata.endTime || 'N/A'}\n`;
405
+ md += `- **Duration:** ${this.formatDuration(metadata.duration || 0)}\n`;
406
+ md += `- **Status:** ${metadata.status}\n\n`;
407
+ // Configuration
408
+ md += `## Configuration\n\n`;
409
+ md += `\`\`\`json\n${JSON.stringify(configuration, null, 2)}\n\`\`\`\n\n`;
410
+ // Execution Plan
411
+ md += `## Execution Plan\n\n`;
412
+ md += `| Item | To Create | Existing | To Develop |\n`;
413
+ md += `|------|-----------|----------|------------|\n`;
414
+ md += `| Epics | ${executionPlan.epicsToCreate} | ${executionPlan.epicsExisting} | - |\n`;
415
+ md += `| Stories | ${executionPlan.storiesToCreate} | ${executionPlan.storiesExisting} | ${executionPlan.storiesToDevelop} |\n`;
416
+ md += `\nEstimated Duration: ${executionPlan.estimatedDuration}\n\n`;
417
+ // Summary
418
+ md += `## Execution Summary\n\n`;
419
+ md += `| Phase | Completed | Failed | Pending |\n`;
420
+ md += `|-------|-----------|--------|----------|\n`;
421
+ md += `| Epics | ${summary.completed.epics} | ${summary.failed.epics} | ${summary.pending.epics} |\n`;
422
+ md += `| Stories | ${summary.completed.stories} | ${summary.failed.stories} | ${summary.pending.stories} |\n`;
423
+ md += `| Development | ${summary.completed.developments} | ${summary.failed.developments} | ${summary.pending.developments} |\n\n`;
424
+ // Errors
425
+ if (errors.length > 0) {
426
+ md += `## Errors (${errors.length})\n\n`;
427
+ for (const error of errors) {
428
+ md += `### ${error.phase} - ${error.identifier}\n\n`;
429
+ md += `- **Time:** ${error.timestamp}\n`;
430
+ md += `- **Error:** ${error.error}\n\n`;
431
+ }
432
+ }
433
+ // Prompts and Responses
434
+ md += `## Prompts Sent (${prompts.length})\n\n`;
435
+ for (const [index, prompt] of prompts.entries()) {
436
+ md += `### ${index + 1}. ${prompt.phase.toUpperCase()} - ${prompt.itemIdentifier}\n\n`;
437
+ md += `- **Agent:** ${prompt.agentType}\n`;
438
+ md += `- **Time:** ${prompt.timestamp}\n`;
439
+ md += `- **Prompt Length:** ${prompt.promptLength} characters\n`;
440
+ md += `- **Timeout:** ${prompt.timeout}ms\n\n`;
441
+ md += `<details>\n<summary>View Prompt</summary>\n\n`;
442
+ md += `\`\`\`\n${prompt.prompt}\n\`\`\`\n\n`;
443
+ md += `</details>\n\n`;
444
+ }
445
+ md += `## Responses Received (${responses.length})\n\n`;
446
+ for (const [index, response] of responses.entries()) {
447
+ md += `### ${index + 1}. ${response.phase.toUpperCase()} - ${response.itemIdentifier}\n\n`;
448
+ md += `- **Agent:** ${response.agentType}\n`;
449
+ md += `- **Time:** ${response.timestamp}\n`;
450
+ md += `- **Success:** ${response.success ? '✓' : '✗'}\n`;
451
+ md += `- **Duration:** ${this.formatDuration(response.duration)}\n`;
452
+ md += `- **Exit Code:** ${response.exitCode}\n`;
453
+ md += `- **Output Length:** ${response.outputLength} characters\n\n`;
454
+ if (response.success) {
455
+ md += `<details>\n<summary>View Output</summary>\n\n`;
456
+ md += `\`\`\`\n${response.output.slice(0, 5000)}${response.outputLength > 5000 ? '\n... (truncated)' : ''}\n\`\`\`\n\n`;
457
+ md += `</details>\n\n`;
458
+ }
459
+ else {
460
+ md += `**Errors:**\n\`\`\`\n${response.errors}\n\`\`\`\n\n`;
461
+ }
462
+ }
463
+ return md;
464
+ }
465
+ /**
466
+ * Format duration in human-readable format
467
+ */
468
+ formatDuration(ms) {
469
+ if (ms < 1000)
470
+ return `${ms}ms`;
471
+ if (ms < 60_000)
472
+ return `${(ms / 1000).toFixed(1)}s`;
473
+ if (ms < 3_600_000)
474
+ return `${(ms / 60_000).toFixed(1)}m`;
475
+ return `${(ms / 3_600_000).toFixed(1)}h`;
476
+ }
477
+ /**
478
+ * Format time as HH:MM:SS
479
+ *
480
+ * @param date - Date to format
481
+ * @returns Formatted time string
482
+ * @private
483
+ */
484
+ formatTime(date) {
485
+ const hours = String(date.getHours()).padStart(2, '0');
486
+ const minutes = String(date.getMinutes()).padStart(2, '0');
487
+ const seconds = String(date.getSeconds()).padStart(2, '0');
488
+ return `${hours}:${minutes}:${seconds}`;
489
+ }
490
+ /**
491
+ * Generate human-readable markdown summary
492
+ */
493
+ async generateMarkdownSummary() {
494
+ const markdownPath = this.logFilePath.replace('.json', '.md');
495
+ const markdown = this.buildMarkdownSummary();
496
+ try {
497
+ await fs.writeFile(markdownPath, markdown, 'utf8');
498
+ this.logger.info({ markdownPath }, 'Generated workflow summary markdown');
499
+ }
500
+ catch (error) {
501
+ this.logger.error({ error: error.message }, 'Failed to write markdown summary');
502
+ }
503
+ }
504
+ /**
505
+ * Generate unique workflow ID
506
+ */
507
+ generateWorkflowId() {
508
+ const timestamp = Date.now();
509
+ const random = Math.random().toString(36).slice(2, 9);
510
+ return `${timestamp}-${random}`;
511
+ }
512
+ /**
513
+ * Log an error
514
+ */
515
+ logError(phase, identifier, error, stack) {
516
+ this.logFile.errors.push({
517
+ error,
518
+ identifier,
519
+ phase,
520
+ stack,
521
+ timestamp: new Date().toISOString(),
522
+ });
523
+ this.addLogEntry(phase, 'error', {
524
+ error,
525
+ identifier,
526
+ });
527
+ }
528
+ /**
529
+ * Pad a number to a specific width with spaces
530
+ *
531
+ * @param num - Number to pad
532
+ * @param width - Target width
533
+ * @returns Padded string
534
+ * @private
535
+ */
536
+ padNumber(num, width) {
537
+ const str = num.toString();
538
+ const padding = ' '.repeat(Math.max(0, width - str.length));
539
+ return padding + str;
540
+ }
541
+ /**
542
+ * Write log file to disk
543
+ */
544
+ async writeLogFile() {
545
+ try {
546
+ await fs.writeFile(this.logFilePath, JSON.stringify(this.logFile, null, 2), 'utf8');
547
+ }
548
+ catch (error) {
549
+ this.logger.error({ error: error.message }, 'Failed to write workflow log file');
550
+ }
551
+ }
552
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Batch Processor Service
3
+ *
4
+ * Handles parallel execution of tasks with configurable concurrency,
5
+ * intervals between batches, and partial failure handling.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const logger = createLogger({ namespace: 'batch' })
10
+ * const processor = new BatchProcessor(5, 1000, logger)
11
+ * const results = await processor.processBatch(items, async (item) => {
12
+ * return await someAsyncOperation(item)
13
+ * }, (info) => {
14
+ * console.log(`Processing batch ${info.currentBatch}/${info.totalBatches}`)
15
+ * })
16
+ * ```
17
+ */
18
+ import type pino from 'pino';
19
+ /**
20
+ * Configuration options for BatchProcessor
21
+ */
22
+ export interface BatchProcessorOptions {
23
+ intervalMs: number;
24
+ maxConcurrency: number;
25
+ }
26
+ /**
27
+ * Processor function type for batch processing
28
+ */
29
+ export type ProcessorFunction<TItem, TResult> = (item: TItem) => Promise<TResult>;
30
+ /**
31
+ * Progress information for batch processing
32
+ */
33
+ export interface ProgressInfo {
34
+ completedItems: number;
35
+ currentBatch: number;
36
+ currentItem: number;
37
+ totalBatches: number;
38
+ totalItems: number;
39
+ }
40
+ /**
41
+ * Progress callback type
42
+ */
43
+ export type ProgressCallback = (info: ProgressInfo) => void;
44
+ /**
45
+ * Result of processing a single item
46
+ */
47
+ export interface BatchResult<TResult> {
48
+ error?: Error;
49
+ result?: TResult;
50
+ success: boolean;
51
+ }
52
+ /**
53
+ * BatchProcessor - Handles parallel batch processing with configurable concurrency
54
+ *
55
+ * Processes items in batches with configurable concurrency limits and intervals
56
+ * between batches. Continues processing even if individual items fail, collecting
57
+ * both successes and failures.
58
+ *
59
+ * @example
60
+ * const processor = new BatchProcessor(5, 1000, logger)
61
+ * const results = await processor.processBatch(items, async (item) => {
62
+ * return await processItem(item)
63
+ * })
64
+ */
65
+ export declare class BatchProcessor {
66
+ private readonly intervalMs;
67
+ private readonly logger;
68
+ private readonly maxConcurrency;
69
+ /**
70
+ * Create a new BatchProcessor instance
71
+ *
72
+ * @param maxConcurrency - Maximum number of concurrent operations (must be positive)
73
+ * @param intervalMs - Milliseconds to wait between batches (must be non-negative)
74
+ * @param logger - Logger instance for structured logging
75
+ * @throws {ValidationError} If maxConcurrency is not positive or intervalMs is negative
76
+ */
77
+ constructor(maxConcurrency: number, intervalMs: number, logger: pino.Logger);
78
+ /**
79
+ * Process items in parallel batches with partial failure handling
80
+ *
81
+ * Divides items into batches of size maxConcurrency, processes each batch
82
+ * with Promise.allSettled, waits intervalMs between batches, and collects
83
+ * all results including both successes and failures.
84
+ *
85
+ * @param items - Array of items to process
86
+ * @param processor - Async function to process each item
87
+ * @param onProgress - Optional callback for progress updates
88
+ * @returns Array of BatchResult objects in original item order
89
+ *
90
+ * @example
91
+ * const results = await processor.processBatch(items, async (item) => {
92
+ * return await processItem(item)
93
+ * }, (info) => {
94
+ * console.log(`Batch ${info.currentBatch}/${info.totalBatches}`)
95
+ * })
96
+ */
97
+ processBatch<TItem, TResult>(items: TItem[], processor: ProcessorFunction<TItem, TResult>, onProgress?: ProgressCallback): Promise<Array<BatchResult<TResult>>>;
98
+ /**
99
+ * Delay execution for specified milliseconds
100
+ *
101
+ * @param ms - Milliseconds to wait
102
+ * @returns Promise that resolves after delay
103
+ */
104
+ private delay;
105
+ /**
106
+ * Split array into batches of specified size
107
+ *
108
+ * @param items - Array of items to split
109
+ * @param batchSize - Maximum items per batch
110
+ * @returns Array of batches
111
+ */
112
+ private splitIntoBatches;
113
+ }