@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.
- package/LICENSE +21 -0
- package/README.md +1017 -0
- package/bin/dev +5 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/show.d.ts +34 -0
- package/dist/commands/config/show.js +108 -0
- package/dist/commands/config/validate.d.ts +29 -0
- package/dist/commands/config/validate.js +131 -0
- package/dist/commands/decompose.d.ts +79 -0
- package/dist/commands/decompose.js +327 -0
- package/dist/commands/demo.d.ts +18 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/epics/create.d.ts +123 -0
- package/dist/commands/epics/create.js +459 -0
- package/dist/commands/epics/list.d.ts +120 -0
- package/dist/commands/epics/list.js +280 -0
- package/dist/commands/hello/index.d.ts +12 -0
- package/dist/commands/hello/index.js +34 -0
- package/dist/commands/hello/world.d.ts +8 -0
- package/dist/commands/hello/world.js +24 -0
- package/dist/commands/prd/fix.d.ts +39 -0
- package/dist/commands/prd/fix.js +140 -0
- package/dist/commands/prd/validate.d.ts +112 -0
- package/dist/commands/prd/validate.js +302 -0
- package/dist/commands/stories/create.d.ts +95 -0
- package/dist/commands/stories/create.js +431 -0
- package/dist/commands/stories/develop.d.ts +91 -0
- package/dist/commands/stories/develop.js +460 -0
- package/dist/commands/stories/list.d.ts +84 -0
- package/dist/commands/stories/list.js +291 -0
- package/dist/commands/stories/move.d.ts +66 -0
- package/dist/commands/stories/move.js +273 -0
- package/dist/commands/stories/qa.d.ts +99 -0
- package/dist/commands/stories/qa.js +530 -0
- package/dist/commands/workflow.d.ts +97 -0
- package/dist/commands/workflow.js +390 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/agent-options.d.ts +50 -0
- package/dist/models/agent-options.js +1 -0
- package/dist/models/agent-result.d.ts +29 -0
- package/dist/models/agent-result.js +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +10 -0
- package/dist/models/phase-result.d.ts +65 -0
- package/dist/models/phase-result.js +7 -0
- package/dist/models/provider.d.ts +28 -0
- package/dist/models/provider.js +18 -0
- package/dist/models/story.d.ts +154 -0
- package/dist/models/story.js +18 -0
- package/dist/models/workflow-config.d.ts +148 -0
- package/dist/models/workflow-config.js +1 -0
- package/dist/models/workflow-result.d.ts +164 -0
- package/dist/models/workflow-result.js +7 -0
- package/dist/services/agents/agent-runner-factory.d.ts +31 -0
- package/dist/services/agents/agent-runner-factory.js +44 -0
- package/dist/services/agents/agent-runner.d.ts +46 -0
- package/dist/services/agents/agent-runner.js +29 -0
- package/dist/services/agents/claude-agent-runner.d.ts +81 -0
- package/dist/services/agents/claude-agent-runner.js +332 -0
- package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
- package/dist/services/agents/gemini-agent-runner.js +350 -0
- package/dist/services/agents/index.d.ts +7 -0
- package/dist/services/agents/index.js +7 -0
- package/dist/services/file-system/file-manager.d.ts +110 -0
- package/dist/services/file-system/file-manager.js +223 -0
- package/dist/services/file-system/glob-matcher.d.ts +75 -0
- package/dist/services/file-system/glob-matcher.js +126 -0
- package/dist/services/file-system/path-resolver.d.ts +183 -0
- package/dist/services/file-system/path-resolver.js +400 -0
- package/dist/services/logging/workflow-logger.d.ts +232 -0
- package/dist/services/logging/workflow-logger.js +552 -0
- package/dist/services/orchestration/batch-processor.d.ts +113 -0
- package/dist/services/orchestration/batch-processor.js +187 -0
- package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
- package/dist/services/orchestration/dependency-graph-executor.js +447 -0
- package/dist/services/orchestration/index.d.ts +10 -0
- package/dist/services/orchestration/index.js +8 -0
- package/dist/services/orchestration/input-detector.d.ts +125 -0
- package/dist/services/orchestration/input-detector.js +381 -0
- package/dist/services/orchestration/story-queue.d.ts +94 -0
- package/dist/services/orchestration/story-queue.js +170 -0
- package/dist/services/orchestration/story-type-detector.d.ts +80 -0
- package/dist/services/orchestration/story-type-detector.js +258 -0
- package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
- package/dist/services/orchestration/task-decomposition-service.js +607 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
- package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
- package/dist/services/parsers/epic-parser.d.ts +117 -0
- package/dist/services/parsers/epic-parser.js +264 -0
- package/dist/services/parsers/prd-fixer.d.ts +86 -0
- package/dist/services/parsers/prd-fixer.js +194 -0
- package/dist/services/parsers/prd-parser.d.ts +123 -0
- package/dist/services/parsers/prd-parser.js +286 -0
- package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
- package/dist/services/parsers/standalone-story-parser.js +255 -0
- package/dist/services/parsers/story-parser-factory.d.ts +81 -0
- package/dist/services/parsers/story-parser-factory.js +108 -0
- package/dist/services/parsers/story-parser.d.ts +122 -0
- package/dist/services/parsers/story-parser.js +262 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
- package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
- package/dist/services/scaffolding/file-scaffolder.js +314 -0
- package/dist/services/validation/config-validator.d.ts +88 -0
- package/dist/services/validation/config-validator.js +167 -0
- package/dist/types/task-graph.d.ts +142 -0
- package/dist/types/task-graph.js +5 -0
- package/dist/utils/colors.d.ts +49 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/error-formatter.d.ts +64 -0
- package/dist/utils/error-formatter.js +279 -0
- package/dist/utils/errors.d.ts +170 -0
- package/dist/utils/errors.js +233 -0
- package/dist/utils/formatters.d.ts +84 -0
- package/dist/utils/formatters.js +162 -0
- package/dist/utils/logger.d.ts +63 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/progress.d.ts +104 -0
- package/dist/utils/progress.js +161 -0
- package/dist/utils/retry.d.ts +114 -0
- package/dist/utils/retry.js +160 -0
- package/dist/utils/shared-flags.d.ts +28 -0
- package/dist/utils/shared-flags.js +43 -0
- 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
|
+
}
|