@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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides centralized logging configuration using Pino.
|
|
5
|
+
* Supports structured JSON logging in production and pretty printing in development.
|
|
6
|
+
*/
|
|
7
|
+
import pino from 'pino';
|
|
8
|
+
/**
|
|
9
|
+
* Log levels supported by the logger
|
|
10
|
+
*/
|
|
11
|
+
export type LogLevel = 'debug' | 'error' | 'fatal' | 'info' | 'silent' | 'trace' | 'warn';
|
|
12
|
+
/**
|
|
13
|
+
* Logger configuration options
|
|
14
|
+
*/
|
|
15
|
+
export interface LoggerOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Minimum log level
|
|
18
|
+
*/
|
|
19
|
+
level?: LogLevel;
|
|
20
|
+
/**
|
|
21
|
+
* Logger namespace (e.g., 'services:parser')
|
|
22
|
+
*/
|
|
23
|
+
namespace?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Enable pretty printing for development
|
|
26
|
+
*/
|
|
27
|
+
pretty?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a logger instance with the specified configuration
|
|
31
|
+
*
|
|
32
|
+
* @param options - Logger configuration options
|
|
33
|
+
* @returns Configured Pino logger instance
|
|
34
|
+
* @example
|
|
35
|
+
* const logger = createLogger({ namespace: 'services:parser', level: 'info' })
|
|
36
|
+
* logger.info('Parsing PRD file')
|
|
37
|
+
*/
|
|
38
|
+
export declare function createLogger(options?: LoggerOptions): pino.Logger<never, boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Create a child logger with additional context
|
|
41
|
+
*
|
|
42
|
+
* @param parent - Parent logger instance
|
|
43
|
+
* @param context - Additional context to include in logs
|
|
44
|
+
* @returns Child logger with merged context
|
|
45
|
+
* @example
|
|
46
|
+
* const baseLogger = createLogger({ namespace: 'services' })
|
|
47
|
+
* const childLogger = createChildLogger(baseLogger, { epic: 'epic-1' })
|
|
48
|
+
* childLogger.info('Processing epic') // Will include epic context
|
|
49
|
+
*/
|
|
50
|
+
export declare function createChildLogger(parent: pino.Logger, context: Record<string, unknown>): pino.Logger<never, boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Generate a correlation ID for tracking a single command invocation
|
|
53
|
+
*
|
|
54
|
+
* @returns Correlation ID in format: req-{timestamp}-{random}
|
|
55
|
+
* @example
|
|
56
|
+
* const correlationId = generateCorrelationId()
|
|
57
|
+
* // Returns: 'req-1234567890-0.5'
|
|
58
|
+
*/
|
|
59
|
+
export declare function generateCorrelationId(): string;
|
|
60
|
+
/**
|
|
61
|
+
* Default logger instance for general use
|
|
62
|
+
*/
|
|
63
|
+
export declare const logger: pino.Logger<never, boolean>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides centralized logging configuration using Pino.
|
|
5
|
+
* Supports structured JSON logging in production and pretty printing in development.
|
|
6
|
+
*/
|
|
7
|
+
import pino from 'pino';
|
|
8
|
+
import pretty from 'pino-pretty';
|
|
9
|
+
/**
|
|
10
|
+
* Get the log level from environment variable or default
|
|
11
|
+
*
|
|
12
|
+
* @param defaultLevel - Default log level to use if LOG_LEVEL is not set
|
|
13
|
+
* @returns Valid log level
|
|
14
|
+
*/
|
|
15
|
+
function getLogLevel(defaultLevel = 'info') {
|
|
16
|
+
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
17
|
+
const validLevels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent'];
|
|
18
|
+
if (envLevel && validLevels.includes(envLevel)) {
|
|
19
|
+
return envLevel;
|
|
20
|
+
}
|
|
21
|
+
return defaultLevel;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a logger instance with the specified configuration
|
|
25
|
+
*
|
|
26
|
+
* @param options - Logger configuration options
|
|
27
|
+
* @returns Configured Pino logger instance
|
|
28
|
+
* @example
|
|
29
|
+
* const logger = createLogger({ namespace: 'services:parser', level: 'info' })
|
|
30
|
+
* logger.info('Parsing PRD file')
|
|
31
|
+
*/
|
|
32
|
+
export function createLogger(options = {}) {
|
|
33
|
+
const { level, namespace, pretty: enablePretty = process.env.NODE_ENV !== 'production' } = options;
|
|
34
|
+
// Use LOG_LEVEL environment variable if no explicit level is provided
|
|
35
|
+
const effectiveLevel = level || getLogLevel('info');
|
|
36
|
+
const baseConfig = {
|
|
37
|
+
level: effectiveLevel,
|
|
38
|
+
name: namespace,
|
|
39
|
+
};
|
|
40
|
+
if (enablePretty) {
|
|
41
|
+
const stream = pretty({
|
|
42
|
+
colorize: true,
|
|
43
|
+
ignore: 'pid,hostname',
|
|
44
|
+
translateTime: 'HH:MM:ss',
|
|
45
|
+
});
|
|
46
|
+
return pino(baseConfig, stream);
|
|
47
|
+
}
|
|
48
|
+
return pino(baseConfig);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a child logger with additional context
|
|
52
|
+
*
|
|
53
|
+
* @param parent - Parent logger instance
|
|
54
|
+
* @param context - Additional context to include in logs
|
|
55
|
+
* @returns Child logger with merged context
|
|
56
|
+
* @example
|
|
57
|
+
* const baseLogger = createLogger({ namespace: 'services' })
|
|
58
|
+
* const childLogger = createChildLogger(baseLogger, { epic: 'epic-1' })
|
|
59
|
+
* childLogger.info('Processing epic') // Will include epic context
|
|
60
|
+
*/
|
|
61
|
+
export function createChildLogger(parent, context) {
|
|
62
|
+
return parent.child(context);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generate a correlation ID for tracking a single command invocation
|
|
66
|
+
*
|
|
67
|
+
* @returns Correlation ID in format: req-{timestamp}-{random}
|
|
68
|
+
* @example
|
|
69
|
+
* const correlationId = generateCorrelationId()
|
|
70
|
+
* // Returns: 'req-1234567890-0.5'
|
|
71
|
+
*/
|
|
72
|
+
export function generateCorrelationId() {
|
|
73
|
+
return `req-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Default logger instance for general use
|
|
77
|
+
*/
|
|
78
|
+
export const logger = createLogger({ namespace: 'bmad-workflow' });
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Ora } from 'ora';
|
|
2
|
+
/**
|
|
3
|
+
* Create a simple spinner for single operations
|
|
4
|
+
*
|
|
5
|
+
* @param text - Initial text to display with spinner
|
|
6
|
+
* @returns Ora spinner instance
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const spinner = createSpinner('Loading data...')
|
|
11
|
+
* spinner.start()
|
|
12
|
+
* // ... do work
|
|
13
|
+
* spinner.succeed('Data loaded successfully')
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare const createSpinner: (text: string) => Ora;
|
|
17
|
+
/**
|
|
18
|
+
* Pipeline status for multi-line progress display
|
|
19
|
+
*/
|
|
20
|
+
export interface PipelineProgressStatus {
|
|
21
|
+
completed: number;
|
|
22
|
+
creating: number;
|
|
23
|
+
developing: number;
|
|
24
|
+
queued: number;
|
|
25
|
+
workers: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Multi-step progress tracker for workflows with multiple phases
|
|
29
|
+
*
|
|
30
|
+
* Manages a sequence of steps with progress indication, automatically
|
|
31
|
+
* handling spinner lifecycle and step transitions.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const progress = new MultiStepProgress(['Parse PRD', 'Create epics', 'Validate'])
|
|
36
|
+
* progress.start(0) // Start first step
|
|
37
|
+
* // ... do work
|
|
38
|
+
* progress.start(1) // Move to second step
|
|
39
|
+
* // ... do work
|
|
40
|
+
* progress.succeed('All steps completed')
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare class MultiStepProgress {
|
|
44
|
+
private currentStep;
|
|
45
|
+
private pipelineMode;
|
|
46
|
+
private spinner;
|
|
47
|
+
private steps;
|
|
48
|
+
/**
|
|
49
|
+
* Create a new multi-step progress tracker
|
|
50
|
+
*
|
|
51
|
+
* @param steps - Array of step descriptions
|
|
52
|
+
*/
|
|
53
|
+
constructor(steps: string[]);
|
|
54
|
+
/**
|
|
55
|
+
* Enable pipeline mode for multi-line progress display
|
|
56
|
+
*/
|
|
57
|
+
enablePipelineMode(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Mark the current workflow as failed
|
|
60
|
+
*
|
|
61
|
+
* @param errorText - Optional error message
|
|
62
|
+
*/
|
|
63
|
+
fail(errorText?: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get the current step index
|
|
66
|
+
*
|
|
67
|
+
* @returns Current step index, or -1 if no step is active
|
|
68
|
+
*/
|
|
69
|
+
getCurrentStep(): number;
|
|
70
|
+
/**
|
|
71
|
+
* Get total number of steps
|
|
72
|
+
*
|
|
73
|
+
* @returns Total number of steps
|
|
74
|
+
*/
|
|
75
|
+
getTotalSteps(): number;
|
|
76
|
+
/**
|
|
77
|
+
* Start a specific step in the workflow
|
|
78
|
+
*
|
|
79
|
+
* @param stepIndex - Zero-based index of the step to start
|
|
80
|
+
*/
|
|
81
|
+
start(stepIndex: number): void;
|
|
82
|
+
/**
|
|
83
|
+
* Stop the spinner without marking success or failure
|
|
84
|
+
*/
|
|
85
|
+
stop(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Mark the current workflow as successful
|
|
88
|
+
*
|
|
89
|
+
* @param finalText - Optional final success message
|
|
90
|
+
*/
|
|
91
|
+
succeed(finalText?: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Update the text of the current step
|
|
94
|
+
*
|
|
95
|
+
* @param text - New text to display
|
|
96
|
+
*/
|
|
97
|
+
update(text: string): void;
|
|
98
|
+
/**
|
|
99
|
+
* Update pipeline status with color-coded multi-line display
|
|
100
|
+
*
|
|
101
|
+
* @param status - Current pipeline status
|
|
102
|
+
*/
|
|
103
|
+
updatePipelineStatus(status: PipelineProgressStatus): void;
|
|
104
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
/**
|
|
3
|
+
* Create a simple spinner for single operations
|
|
4
|
+
*
|
|
5
|
+
* @param text - Initial text to display with spinner
|
|
6
|
+
* @returns Ora spinner instance
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const spinner = createSpinner('Loading data...')
|
|
11
|
+
* spinner.start()
|
|
12
|
+
* // ... do work
|
|
13
|
+
* spinner.succeed('Data loaded successfully')
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const createSpinner = (text) => ora({
|
|
17
|
+
color: 'cyan',
|
|
18
|
+
text,
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Multi-step progress tracker for workflows with multiple phases
|
|
22
|
+
*
|
|
23
|
+
* Manages a sequence of steps with progress indication, automatically
|
|
24
|
+
* handling spinner lifecycle and step transitions.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const progress = new MultiStepProgress(['Parse PRD', 'Create epics', 'Validate'])
|
|
29
|
+
* progress.start(0) // Start first step
|
|
30
|
+
* // ... do work
|
|
31
|
+
* progress.start(1) // Move to second step
|
|
32
|
+
* // ... do work
|
|
33
|
+
* progress.succeed('All steps completed')
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class MultiStepProgress {
|
|
37
|
+
currentStep = -1;
|
|
38
|
+
pipelineMode = false;
|
|
39
|
+
spinner = null;
|
|
40
|
+
steps;
|
|
41
|
+
/**
|
|
42
|
+
* Create a new multi-step progress tracker
|
|
43
|
+
*
|
|
44
|
+
* @param steps - Array of step descriptions
|
|
45
|
+
*/
|
|
46
|
+
constructor(steps) {
|
|
47
|
+
this.steps = steps;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Enable pipeline mode for multi-line progress display
|
|
51
|
+
*/
|
|
52
|
+
enablePipelineMode() {
|
|
53
|
+
this.pipelineMode = true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Mark the current workflow as failed
|
|
57
|
+
*
|
|
58
|
+
* @param errorText - Optional error message
|
|
59
|
+
*/
|
|
60
|
+
fail(errorText) {
|
|
61
|
+
if (this.spinner) {
|
|
62
|
+
this.spinner.fail(errorText || 'Workflow failed');
|
|
63
|
+
this.spinner = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the current step index
|
|
68
|
+
*
|
|
69
|
+
* @returns Current step index, or -1 if no step is active
|
|
70
|
+
*/
|
|
71
|
+
getCurrentStep() {
|
|
72
|
+
return this.currentStep;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get total number of steps
|
|
76
|
+
*
|
|
77
|
+
* @returns Total number of steps
|
|
78
|
+
*/
|
|
79
|
+
getTotalSteps() {
|
|
80
|
+
return this.steps.length;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Start a specific step in the workflow
|
|
84
|
+
*
|
|
85
|
+
* @param stepIndex - Zero-based index of the step to start
|
|
86
|
+
*/
|
|
87
|
+
start(stepIndex) {
|
|
88
|
+
// Stop previous spinner if exists
|
|
89
|
+
if (this.spinner) {
|
|
90
|
+
this.spinner.stop();
|
|
91
|
+
}
|
|
92
|
+
if (stepIndex < 0 || stepIndex >= this.steps.length) {
|
|
93
|
+
throw new Error(`Invalid step index: ${stepIndex}`);
|
|
94
|
+
}
|
|
95
|
+
this.currentStep = stepIndex;
|
|
96
|
+
const stepText = `[${stepIndex + 1}/${this.steps.length}] ${this.steps[stepIndex]}`;
|
|
97
|
+
this.spinner = ora({
|
|
98
|
+
color: 'cyan',
|
|
99
|
+
text: stepText,
|
|
100
|
+
});
|
|
101
|
+
this.spinner.start();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Stop the spinner without marking success or failure
|
|
105
|
+
*/
|
|
106
|
+
stop() {
|
|
107
|
+
if (this.spinner) {
|
|
108
|
+
this.spinner.stop();
|
|
109
|
+
this.spinner = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Mark the current workflow as successful
|
|
114
|
+
*
|
|
115
|
+
* @param finalText - Optional final success message
|
|
116
|
+
*/
|
|
117
|
+
succeed(finalText) {
|
|
118
|
+
if (this.spinner) {
|
|
119
|
+
this.spinner.succeed(finalText || 'All steps completed');
|
|
120
|
+
this.spinner = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Update the text of the current step
|
|
125
|
+
*
|
|
126
|
+
* @param text - New text to display
|
|
127
|
+
*/
|
|
128
|
+
update(text) {
|
|
129
|
+
if (this.spinner) {
|
|
130
|
+
this.spinner.text = text;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Update pipeline status with color-coded multi-line display
|
|
135
|
+
*
|
|
136
|
+
* @param status - Current pipeline status
|
|
137
|
+
*/
|
|
138
|
+
updatePipelineStatus(status) {
|
|
139
|
+
if (!this.spinner || !this.pipelineMode) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const lines = [];
|
|
143
|
+
// Phase 1: Story Creation (cyan)
|
|
144
|
+
if (status.creating > 0) {
|
|
145
|
+
lines.push(` ✓ Creating: ${status.creating}`);
|
|
146
|
+
}
|
|
147
|
+
// Phase 2: Queue (yellow)
|
|
148
|
+
if (status.queued > 0) {
|
|
149
|
+
lines.push(` ⏳ Queued: ${status.queued}`);
|
|
150
|
+
}
|
|
151
|
+
// Phase 3: Development (blue) with worker count
|
|
152
|
+
if (status.developing > 0 || status.workers > 0) {
|
|
153
|
+
lines.push(` 🔄 Developing: ${status.developing} (Workers: ${status.workers})`);
|
|
154
|
+
}
|
|
155
|
+
// Phase 4: Completed (green)
|
|
156
|
+
if (status.completed > 0) {
|
|
157
|
+
lines.push(` ✅ Completed: ${status.completed}`);
|
|
158
|
+
}
|
|
159
|
+
this.spinner.text = lines.length > 0 ? lines.join('\n') : 'Pipeline running...';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Strategy Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides configurable retry logic with exponential backoff for handling
|
|
5
|
+
* transient failures gracefully.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
/**
|
|
9
|
+
* Options for configuring retry behavior
|
|
10
|
+
*/
|
|
11
|
+
export interface RetryOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Initial backoff delay in milliseconds
|
|
14
|
+
* @default 1000 (1 second)
|
|
15
|
+
*/
|
|
16
|
+
backoffMs?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Backoff multiplier for exponential backoff
|
|
19
|
+
* @default 2 (doubles each retry)
|
|
20
|
+
*/
|
|
21
|
+
backoffMultiplier?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Predicate function to determine if error is retryable
|
|
24
|
+
* Returns true if the error should trigger a retry
|
|
25
|
+
* @default () => true (all errors are retryable)
|
|
26
|
+
*/
|
|
27
|
+
isRetryable?: (error: Error) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Logger instance for retry logging
|
|
30
|
+
* If not provided, a default logger will be created
|
|
31
|
+
*/
|
|
32
|
+
logger?: pino.Logger;
|
|
33
|
+
/**
|
|
34
|
+
* Maximum number of retry attempts
|
|
35
|
+
* @default 3
|
|
36
|
+
*/
|
|
37
|
+
maxRetries?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Retry strategy implementation with configurable exponential backoff
|
|
41
|
+
*
|
|
42
|
+
* Automatically retries failed operations with increasing delays between attempts.
|
|
43
|
+
* Supports custom predicates to determine if errors are retryable.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const retry = new RetryStrategy({ maxRetries: 3, backoffMs: 1000 })
|
|
47
|
+
* const result = await retry.execute(async () => {
|
|
48
|
+
* // operation that might fail transiently
|
|
49
|
+
* })
|
|
50
|
+
*/
|
|
51
|
+
export declare class RetryStrategy {
|
|
52
|
+
private readonly backoffMs;
|
|
53
|
+
private readonly backoffMultiplier;
|
|
54
|
+
private readonly isRetryable;
|
|
55
|
+
private readonly logger;
|
|
56
|
+
private readonly maxRetries;
|
|
57
|
+
/**
|
|
58
|
+
* Create a new RetryStrategy
|
|
59
|
+
*
|
|
60
|
+
* @param options - Configuration options for retry behavior
|
|
61
|
+
* @example
|
|
62
|
+
* const retry = new RetryStrategy({
|
|
63
|
+
* maxRetries: 3,
|
|
64
|
+
* backoffMs: 1000,
|
|
65
|
+
* isRetryable: (error) => error.message.includes('timeout')
|
|
66
|
+
* })
|
|
67
|
+
*/
|
|
68
|
+
constructor(options?: RetryOptions);
|
|
69
|
+
/**
|
|
70
|
+
* Execute an async operation with retry logic
|
|
71
|
+
*
|
|
72
|
+
* Attempts to execute the operation, retrying on failure with exponential backoff
|
|
73
|
+
* if the error is determined to be retryable.
|
|
74
|
+
*
|
|
75
|
+
* @param operation - Async function to execute
|
|
76
|
+
* @returns Promise resolving to the operation result
|
|
77
|
+
* @throws Error if all retry attempts are exhausted
|
|
78
|
+
* @example
|
|
79
|
+
* const result = await retry.execute(async () => {
|
|
80
|
+
* return await fetchData()
|
|
81
|
+
* })
|
|
82
|
+
*/
|
|
83
|
+
execute<T>(operation: () => Promise<T>): Promise<T>;
|
|
84
|
+
/**
|
|
85
|
+
* Calculate exponential backoff delay
|
|
86
|
+
*
|
|
87
|
+
* @param attempt - Current attempt number (1-indexed)
|
|
88
|
+
* @returns Delay in milliseconds
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
private calculateBackoff;
|
|
92
|
+
/**
|
|
93
|
+
* Delay execution for specified milliseconds
|
|
94
|
+
*
|
|
95
|
+
* @param ms - Milliseconds to delay
|
|
96
|
+
* @returns Promise that resolves after delay
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
private delay;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Helper function to create a retry predicate for Claude CLI errors
|
|
103
|
+
*
|
|
104
|
+
* Returns true if the error represents a transient failure that should be retried.
|
|
105
|
+
* Specifically checks for timeout (exit code 124) and killed (exit code 137) errors.
|
|
106
|
+
*
|
|
107
|
+
* @param error - Error to check
|
|
108
|
+
* @returns True if error is retryable
|
|
109
|
+
* @example
|
|
110
|
+
* const retry = new RetryStrategy({
|
|
111
|
+
* isRetryable: isClaudeCliRetryable
|
|
112
|
+
* })
|
|
113
|
+
*/
|
|
114
|
+
export declare function isClaudeCliRetryable(error: Error): boolean;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Strategy Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides configurable retry logic with exponential backoff for handling
|
|
5
|
+
* transient failures gracefully.
|
|
6
|
+
*/
|
|
7
|
+
import { createLogger } from './logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Retry strategy implementation with configurable exponential backoff
|
|
10
|
+
*
|
|
11
|
+
* Automatically retries failed operations with increasing delays between attempts.
|
|
12
|
+
* Supports custom predicates to determine if errors are retryable.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const retry = new RetryStrategy({ maxRetries: 3, backoffMs: 1000 })
|
|
16
|
+
* const result = await retry.execute(async () => {
|
|
17
|
+
* // operation that might fail transiently
|
|
18
|
+
* })
|
|
19
|
+
*/
|
|
20
|
+
export class RetryStrategy {
|
|
21
|
+
backoffMs;
|
|
22
|
+
backoffMultiplier;
|
|
23
|
+
isRetryable;
|
|
24
|
+
logger;
|
|
25
|
+
maxRetries;
|
|
26
|
+
/**
|
|
27
|
+
* Create a new RetryStrategy
|
|
28
|
+
*
|
|
29
|
+
* @param options - Configuration options for retry behavior
|
|
30
|
+
* @example
|
|
31
|
+
* const retry = new RetryStrategy({
|
|
32
|
+
* maxRetries: 3,
|
|
33
|
+
* backoffMs: 1000,
|
|
34
|
+
* isRetryable: (error) => error.message.includes('timeout')
|
|
35
|
+
* })
|
|
36
|
+
*/
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
39
|
+
this.backoffMs = options.backoffMs ?? 1000;
|
|
40
|
+
this.backoffMultiplier = options.backoffMultiplier ?? 2;
|
|
41
|
+
this.isRetryable = options.isRetryable ?? (() => true);
|
|
42
|
+
this.logger = options.logger ?? createLogger({ namespace: 'utils:retry' });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Execute an async operation with retry logic
|
|
46
|
+
*
|
|
47
|
+
* Attempts to execute the operation, retrying on failure with exponential backoff
|
|
48
|
+
* if the error is determined to be retryable.
|
|
49
|
+
*
|
|
50
|
+
* @param operation - Async function to execute
|
|
51
|
+
* @returns Promise resolving to the operation result
|
|
52
|
+
* @throws Error if all retry attempts are exhausted
|
|
53
|
+
* @example
|
|
54
|
+
* const result = await retry.execute(async () => {
|
|
55
|
+
* return await fetchData()
|
|
56
|
+
* })
|
|
57
|
+
*/
|
|
58
|
+
async execute(operation) {
|
|
59
|
+
let lastError;
|
|
60
|
+
let attempt = 0;
|
|
61
|
+
// Retry loop must be sequential to respect retry delays and attempt ordering
|
|
62
|
+
while (attempt <= this.maxRetries) {
|
|
63
|
+
try {
|
|
64
|
+
// Attempt the operation
|
|
65
|
+
// eslint-disable-next-line no-await-in-loop -- sequential retry attempts required
|
|
66
|
+
const result = await operation();
|
|
67
|
+
// Success - log if this was a retry
|
|
68
|
+
if (attempt > 0) {
|
|
69
|
+
this.logger.info({
|
|
70
|
+
attempt,
|
|
71
|
+
maxRetries: this.maxRetries,
|
|
72
|
+
}, 'Operation succeeded after retry');
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
lastError = error;
|
|
78
|
+
attempt++;
|
|
79
|
+
// Check if we should retry
|
|
80
|
+
if (attempt > this.maxRetries) {
|
|
81
|
+
// Exhausted all retries
|
|
82
|
+
this.logger.error({
|
|
83
|
+
attempt: attempt - 1,
|
|
84
|
+
errorMessage: lastError.message,
|
|
85
|
+
maxRetries: this.maxRetries,
|
|
86
|
+
}, 'Operation failed after exhausting all retries');
|
|
87
|
+
throw lastError;
|
|
88
|
+
}
|
|
89
|
+
// Check if error is retryable
|
|
90
|
+
if (!this.isRetryable(lastError)) {
|
|
91
|
+
this.logger.warn({
|
|
92
|
+
attempt: attempt - 1,
|
|
93
|
+
errorMessage: lastError.message,
|
|
94
|
+
}, 'Operation failed with non-retryable error');
|
|
95
|
+
throw lastError;
|
|
96
|
+
}
|
|
97
|
+
// Calculate backoff delay
|
|
98
|
+
const delay = this.calculateBackoff(attempt);
|
|
99
|
+
this.logger.warn({
|
|
100
|
+
attempt,
|
|
101
|
+
delayMs: delay,
|
|
102
|
+
errorMessage: lastError.message,
|
|
103
|
+
maxRetries: this.maxRetries,
|
|
104
|
+
}, 'Operation failed, retrying after delay');
|
|
105
|
+
// Wait before retrying - must be sequential to enforce retry delay
|
|
106
|
+
// eslint-disable-next-line no-await-in-loop -- retry delay must be sequential
|
|
107
|
+
await this.delay(delay);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// This should never be reached, but TypeScript needs it
|
|
111
|
+
throw lastError || new Error('Operation failed without error');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculate exponential backoff delay
|
|
115
|
+
*
|
|
116
|
+
* @param attempt - Current attempt number (1-indexed)
|
|
117
|
+
* @returns Delay in milliseconds
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
calculateBackoff(attempt) {
|
|
121
|
+
// Exponential backoff: backoffMs * (multiplier ^ (attempt - 1))
|
|
122
|
+
// For default values (1000ms, multiplier 2): 1s, 2s, 4s, 8s...
|
|
123
|
+
return this.backoffMs * this.backoffMultiplier ** (attempt - 1);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Delay execution for specified milliseconds
|
|
127
|
+
*
|
|
128
|
+
* @param ms - Milliseconds to delay
|
|
129
|
+
* @returns Promise that resolves after delay
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
async delay(ms) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
setTimeout(resolve, ms);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Helper function to create a retry predicate for Claude CLI errors
|
|
140
|
+
*
|
|
141
|
+
* Returns true if the error represents a transient failure that should be retried.
|
|
142
|
+
* Specifically checks for timeout (exit code 124) and killed (exit code 137) errors.
|
|
143
|
+
*
|
|
144
|
+
* @param error - Error to check
|
|
145
|
+
* @returns True if error is retryable
|
|
146
|
+
* @example
|
|
147
|
+
* const retry = new RetryStrategy({
|
|
148
|
+
* isRetryable: isClaudeCliRetryable
|
|
149
|
+
* })
|
|
150
|
+
*/
|
|
151
|
+
export function isClaudeCliRetryable(error) {
|
|
152
|
+
// Check if error has exitCode property (AgentError)
|
|
153
|
+
const exitCode = error.context?.exitCode;
|
|
154
|
+
if (exitCode !== undefined) {
|
|
155
|
+
// Retry on timeout (124) or killed (137)
|
|
156
|
+
return exitCode === 124 || exitCode === 137;
|
|
157
|
+
}
|
|
158
|
+
// Default to not retryable if we can't determine exit code
|
|
159
|
+
return false;
|
|
160
|
+
}
|