@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,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
+ }