@eldrforge/tree-execution 0.1.0

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 (79) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +130 -0
  3. package/dist/TreeExecutor.d.ts +113 -0
  4. package/dist/TreeExecutor.d.ts.map +1 -0
  5. package/dist/TreeExecutor.js +113 -0
  6. package/dist/TreeExecutor.js.map +1 -0
  7. package/dist/checkpoint/CheckpointManager.d.ts +18 -0
  8. package/dist/checkpoint/CheckpointManager.d.ts.map +1 -0
  9. package/dist/checkpoint/CheckpointManager.js +156 -0
  10. package/dist/checkpoint/CheckpointManager.js.map +1 -0
  11. package/dist/checkpoint/index.d.ts +5 -0
  12. package/dist/checkpoint/index.d.ts.map +1 -0
  13. package/dist/checkpoint/index.js +5 -0
  14. package/dist/checkpoint/index.js.map +1 -0
  15. package/dist/execution/CommandValidator.d.ts +25 -0
  16. package/dist/execution/CommandValidator.d.ts.map +1 -0
  17. package/dist/execution/CommandValidator.js +129 -0
  18. package/dist/execution/CommandValidator.js.map +1 -0
  19. package/dist/execution/DependencyChecker.d.ts +47 -0
  20. package/dist/execution/DependencyChecker.d.ts.map +1 -0
  21. package/dist/execution/DependencyChecker.js +95 -0
  22. package/dist/execution/DependencyChecker.js.map +1 -0
  23. package/dist/execution/DynamicTaskPool.d.ts +118 -0
  24. package/dist/execution/DynamicTaskPool.d.ts.map +1 -0
  25. package/dist/execution/DynamicTaskPool.js +658 -0
  26. package/dist/execution/DynamicTaskPool.js.map +1 -0
  27. package/dist/execution/RecoveryManager.d.ts +89 -0
  28. package/dist/execution/RecoveryManager.d.ts.map +1 -0
  29. package/dist/execution/RecoveryManager.js +592 -0
  30. package/dist/execution/RecoveryManager.js.map +1 -0
  31. package/dist/execution/ResourceMonitor.d.ts +73 -0
  32. package/dist/execution/ResourceMonitor.d.ts.map +1 -0
  33. package/dist/execution/ResourceMonitor.js +148 -0
  34. package/dist/execution/ResourceMonitor.js.map +1 -0
  35. package/dist/execution/Scheduler.d.ts +36 -0
  36. package/dist/execution/Scheduler.d.ts.map +1 -0
  37. package/dist/execution/Scheduler.js +83 -0
  38. package/dist/execution/Scheduler.js.map +1 -0
  39. package/dist/execution/TreeExecutionAdapter.d.ts +45 -0
  40. package/dist/execution/TreeExecutionAdapter.d.ts.map +1 -0
  41. package/dist/execution/TreeExecutionAdapter.js +249 -0
  42. package/dist/execution/TreeExecutionAdapter.js.map +1 -0
  43. package/dist/index.d.ts +29 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +25 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/tree.d.ts +13 -0
  48. package/dist/tree.d.ts.map +1 -0
  49. package/dist/tree.js +2453 -0
  50. package/dist/tree.js.map +1 -0
  51. package/dist/types/config.d.ts +172 -0
  52. package/dist/types/config.d.ts.map +1 -0
  53. package/dist/types/config.js +2 -0
  54. package/dist/types/config.js.map +1 -0
  55. package/dist/types/index.d.ts +6 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/index.js +6 -0
  58. package/dist/types/index.js.map +1 -0
  59. package/dist/types/parallelExecution.d.ts +108 -0
  60. package/dist/types/parallelExecution.d.ts.map +1 -0
  61. package/dist/types/parallelExecution.js +2 -0
  62. package/dist/types/parallelExecution.js.map +1 -0
  63. package/dist/util/commandStubs.d.ts +22 -0
  64. package/dist/util/commandStubs.d.ts.map +1 -0
  65. package/dist/util/commandStubs.js +49 -0
  66. package/dist/util/commandStubs.js.map +1 -0
  67. package/dist/util/logger.d.ts +14 -0
  68. package/dist/util/logger.d.ts.map +1 -0
  69. package/dist/util/logger.js +15 -0
  70. package/dist/util/logger.js.map +1 -0
  71. package/dist/util/mutex.d.ts +38 -0
  72. package/dist/util/mutex.d.ts.map +1 -0
  73. package/dist/util/mutex.js +101 -0
  74. package/dist/util/mutex.js.map +1 -0
  75. package/dist/util/treeUtils.d.ts +46 -0
  76. package/dist/util/treeUtils.d.ts.map +1 -0
  77. package/dist/util/treeUtils.js +74 -0
  78. package/dist/util/treeUtils.js.map +1 -0
  79. package/package.json +64 -0
@@ -0,0 +1,658 @@
1
+ import { EventEmitter } from 'events';
2
+ import { randomUUID } from 'crypto';
3
+ import { getLogger } from '../util/logger.js';
4
+ import { findAllDependents } from '@eldrforge/tree-core';
5
+ import { CheckpointManager } from '../checkpoint/index.js';
6
+ import { DependencyChecker } from './DependencyChecker.js';
7
+ import { ResourceMonitor } from './ResourceMonitor.js';
8
+ import { Scheduler } from './Scheduler.js';
9
+ /**
10
+ * DynamicTaskPool manages parallel execution of packages with dependency awareness
11
+ */
12
+ export class DynamicTaskPool extends EventEmitter {
13
+ config;
14
+ graph;
15
+ state;
16
+ dependencyChecker;
17
+ resourceMonitor;
18
+ scheduler;
19
+ checkpointManager;
20
+ logger = getLogger();
21
+ // Execution tracking
22
+ executionId;
23
+ startTime;
24
+ runningTasks = new Map();
25
+ packageStartTimes = new Map();
26
+ packageEndTimes = new Map();
27
+ packageDurations = new Map();
28
+ retryAttempts = new Map();
29
+ publishedVersions = [];
30
+ constructor(config) {
31
+ super();
32
+ this.config = config;
33
+ this.graph = config.graph;
34
+ this.executionId = randomUUID();
35
+ this.startTime = new Date();
36
+ // Initialize components
37
+ this.dependencyChecker = new DependencyChecker(this.graph);
38
+ this.resourceMonitor = new ResourceMonitor(config.maxConcurrency);
39
+ this.scheduler = new Scheduler(this.graph, this.dependencyChecker);
40
+ this.checkpointManager = new CheckpointManager(config.checkpointPath || process.cwd());
41
+ // Initialize state
42
+ this.state = this.initializeState();
43
+ }
44
+ /**
45
+ * Main execution entry point
46
+ */
47
+ async execute() {
48
+ this.logger.info(`EXECUTION_STARTING: Starting parallel execution | Max Concurrency: ${this.config.maxConcurrency} | Mode: parallel | Purpose: Execute packages with dependency awareness`);
49
+ this.emit('execution:started', { totalPackages: this.graph.packages.size });
50
+ try {
51
+ // Load checkpoint if continuing
52
+ if (this.config.continue) {
53
+ await this.loadCheckpoint();
54
+ }
55
+ // Initialize ready queue
56
+ this.updateReadyQueue();
57
+ // Main execution loop
58
+ while (!this.isComplete()) {
59
+ // Schedule as many packages as we can
60
+ const availableSlots = this.resourceMonitor.getAvailableSlots();
61
+ if (availableSlots > 0 && this.state.ready.length > 0) {
62
+ const toSchedule = this.scheduler.getNext(availableSlots, this.state);
63
+ for (const packageName of toSchedule) {
64
+ await this.schedulePackage(packageName);
65
+ }
66
+ }
67
+ // Check if we're stuck
68
+ if (this.runningTasks.size === 0) {
69
+ if (this.state.ready.length > 0) {
70
+ throw new Error('Deadlock detected: packages ready but cannot execute');
71
+ }
72
+ break; // No more work to do
73
+ }
74
+ // Wait for next package to complete
75
+ const completedTask = await this.waitForNext();
76
+ await this.handleTaskCompletion(completedTask);
77
+ // Update ready queue
78
+ this.updateReadyQueue();
79
+ // Save checkpoint periodically
80
+ if (this.shouldCheckpoint()) {
81
+ await this.saveCheckpoint();
82
+ }
83
+ }
84
+ // Final checkpoint and cleanup
85
+ // Only cleanup if everything completed (no failures, no skipped packages due to dependencies)
86
+ // Note: skippedNoChanges is OK - those packages successfully ran but had nothing to do
87
+ const allCompleted = this.state.failed.length === 0 && this.state.skipped.length === 0;
88
+ if (allCompleted) {
89
+ await this.checkpointManager.cleanup();
90
+ }
91
+ else {
92
+ await this.saveCheckpoint();
93
+ }
94
+ // Build and return result
95
+ const result = this.buildExecutionResult();
96
+ this.emit('execution:completed', { result });
97
+ return result;
98
+ }
99
+ catch (error) {
100
+ // Save checkpoint on error
101
+ await this.saveCheckpoint();
102
+ throw error;
103
+ }
104
+ }
105
+ /**
106
+ * Initialize execution state
107
+ */
108
+ initializeState() {
109
+ const buildOrder = Array.from(this.graph.packages.keys());
110
+ return {
111
+ pending: [...buildOrder],
112
+ ready: [],
113
+ running: [],
114
+ completed: [],
115
+ failed: [],
116
+ skipped: [],
117
+ skippedNoChanges: []
118
+ };
119
+ }
120
+ /**
121
+ * Schedule a package for execution
122
+ */
123
+ async schedulePackage(packageName) {
124
+ // Move from ready to running
125
+ this.state.ready = this.state.ready.filter(p => p !== packageName);
126
+ // Allocate resource
127
+ if (!this.resourceMonitor.allocate()) {
128
+ throw new Error(`Failed to allocate resource for ${packageName}`);
129
+ }
130
+ // Record start time
131
+ this.packageStartTimes.set(packageName, new Date());
132
+ // Create abort controller
133
+ const controller = new AbortController();
134
+ // Start execution
135
+ const promise = this.executePackage(packageName, controller.signal);
136
+ // Track running task
137
+ const task = {
138
+ packageName,
139
+ startTime: new Date(),
140
+ promise,
141
+ controller
142
+ };
143
+ this.runningTasks.set(packageName, task);
144
+ // Update state
145
+ this.state.running.push({
146
+ name: packageName,
147
+ startTime: task.startTime.toISOString(),
148
+ elapsedTime: 0
149
+ });
150
+ // Emit event
151
+ this.emit('package:started', { packageName });
152
+ this.logger.verbose(`Scheduled ${packageName} (${this.runningTasks.size}/${this.config.maxConcurrency} slots used)`);
153
+ }
154
+ /**
155
+ * Execute a single package (placeholder - will be overridden or use callback)
156
+ */
157
+ async executePackage(_packageName, _signal) {
158
+ // This is a placeholder that will be replaced with actual execution logic
159
+ // In the real implementation, this would call the tree.ts executePackage function
160
+ throw new Error('executePackage must be implemented');
161
+ }
162
+ /**
163
+ * Wait for next task to complete
164
+ */
165
+ async waitForNext() {
166
+ const runningTasks = Array.from(this.runningTasks.entries());
167
+ const promises = runningTasks.map(([name, task]) => task.promise
168
+ .then(result => ({ packageName: name, result, error: null }))
169
+ .catch(error => ({ packageName: name, result: null, error })));
170
+ return await Promise.race(promises);
171
+ }
172
+ /**
173
+ * Handle task completion
174
+ */
175
+ async handleTaskCompletion(task) {
176
+ const { packageName, result, error } = task;
177
+ // Remove from running
178
+ this.runningTasks.delete(packageName);
179
+ this.state.running = this.state.running.filter(r => r.name !== packageName);
180
+ this.resourceMonitor.release();
181
+ // Record timing
182
+ const endTime = new Date();
183
+ this.packageEndTimes.set(packageName, endTime);
184
+ const startTime = this.packageStartTimes.get(packageName);
185
+ const duration = endTime.getTime() - startTime.getTime();
186
+ this.packageDurations.set(packageName, duration);
187
+ if (error) {
188
+ await this.handleFailure(packageName, error);
189
+ }
190
+ else {
191
+ await this.handleSuccess(packageName, result);
192
+ }
193
+ }
194
+ /**
195
+ * Handle successful package completion
196
+ */
197
+ async handleSuccess(packageName, result) {
198
+ // Check if this was skipped due to no changes
199
+ if (result.skippedNoChanges) {
200
+ this.state.skippedNoChanges.push(packageName);
201
+ const duration = this.packageDurations.get(packageName);
202
+ this.logger.info(`PACKAGE_SKIPPED_NO_CHANGES: Package skipped due to no code changes | Package: ${packageName} | Duration: ${this.formatDuration(duration)} | Reason: no-changes`);
203
+ this.emit('package:skipped-no-changes', { packageName, result });
204
+ }
205
+ else {
206
+ this.state.completed.push(packageName);
207
+ const duration = this.packageDurations.get(packageName);
208
+ this.logger.info(`PACKAGE_EXECUTION_COMPLETE: Package execution finished successfully | Package: ${packageName} | Duration: ${this.formatDuration(duration)} | Status: success`);
209
+ this.emit('package:completed', { packageName, result });
210
+ // Track published version if applicable
211
+ if (result.publishedVersion) {
212
+ this.publishedVersions.push({
213
+ name: packageName,
214
+ version: result.publishedVersion,
215
+ time: new Date()
216
+ });
217
+ }
218
+ }
219
+ }
220
+ /**
221
+ * Handle package failure
222
+ */
223
+ async handleFailure(packageName, error) {
224
+ const attemptNumber = (this.retryAttempts.get(packageName) || 0) + 1;
225
+ this.retryAttempts.set(packageName, attemptNumber);
226
+ const isRetriable = this.isRetriableError(error);
227
+ const maxRetries = this.config.maxRetries || 3;
228
+ const canRetry = isRetriable && attemptNumber < maxRetries;
229
+ if (canRetry) {
230
+ // Schedule retry
231
+ this.logger.warn(`⟳ ${packageName} failed (attempt ${attemptNumber}/${maxRetries}), will retry`);
232
+ this.state.pending.push(packageName);
233
+ this.emit('package:retrying', { packageName, attemptNumber });
234
+ // Apply backoff delay
235
+ const delay = this.calculateRetryDelay(attemptNumber);
236
+ await new Promise(resolve => setTimeout(resolve, delay));
237
+ }
238
+ else {
239
+ // Permanent failure
240
+ const dependencies = Array.from(this.graph.edges.get(packageName) || []);
241
+ const dependents = Array.from(findAllDependents(packageName, this.graph));
242
+ // Extract detailed error information
243
+ const errorDetails = this.extractErrorDetails(error, packageName);
244
+ const failureInfo = {
245
+ name: packageName,
246
+ error: error.message,
247
+ stack: error.stack,
248
+ isRetriable,
249
+ attemptNumber,
250
+ failedAt: new Date().toISOString(),
251
+ dependencies,
252
+ dependents,
253
+ errorDetails
254
+ };
255
+ this.state.failed.push(failureInfo);
256
+ this.logger.error(`PACKAGE_FAILED_PERMANENT: Package failed permanently | Package: ${packageName} | Error: ${error.message} | Status: failed | Retriable: false`);
257
+ this.emit('package:failed', { packageName, error });
258
+ // Cascade failure to dependents
259
+ await this.cascadeFailure(packageName);
260
+ }
261
+ }
262
+ /**
263
+ * Cascade failure to dependent packages
264
+ */
265
+ async cascadeFailure(failedPackage) {
266
+ const toSkip = findAllDependents(failedPackage, this.graph);
267
+ for (const dependent of toSkip) {
268
+ // Remove from pending/ready
269
+ this.state.pending = this.state.pending.filter(p => p !== dependent);
270
+ this.state.ready = this.state.ready.filter(p => p !== dependent);
271
+ // Add to skipped
272
+ if (!this.state.skipped.includes(dependent)) {
273
+ this.state.skipped.push(dependent);
274
+ this.logger.warn(`PACKAGE_SKIPPED_DEPENDENCY: Package skipped due to failed dependency | Package: ${dependent} | Failed Dependency: ${failedPackage} | Reason: dependency-failed`);
275
+ this.emit('package:skipped', {
276
+ packageName: dependent,
277
+ reason: `Depends on failed ${failedPackage}`
278
+ });
279
+ }
280
+ }
281
+ }
282
+ /**
283
+ * Update ready queue
284
+ */
285
+ updateReadyQueue() {
286
+ const nowReady = [];
287
+ for (const packageName of this.state.pending) {
288
+ if (this.dependencyChecker.isReady(packageName, this.state)) {
289
+ nowReady.push(packageName);
290
+ }
291
+ }
292
+ for (const packageName of nowReady) {
293
+ this.state.pending = this.state.pending.filter(p => p !== packageName);
294
+ this.state.ready.push(packageName);
295
+ }
296
+ }
297
+ /**
298
+ * Check if execution is complete
299
+ */
300
+ isComplete() {
301
+ return (this.state.pending.length === 0 &&
302
+ this.state.ready.length === 0 &&
303
+ this.runningTasks.size === 0);
304
+ }
305
+ /**
306
+ * Determine if should save checkpoint
307
+ */
308
+ shouldCheckpoint() {
309
+ // Checkpoint after each completion for now
310
+ // Could be optimized to checkpoint less frequently
311
+ return true;
312
+ }
313
+ /**
314
+ * Save checkpoint
315
+ */
316
+ async saveCheckpoint() {
317
+ const checkpoint = {
318
+ version: '1.0.0',
319
+ executionId: this.executionId,
320
+ createdAt: this.startTime.toISOString(),
321
+ lastUpdated: new Date().toISOString(),
322
+ command: this.config.command,
323
+ originalConfig: this.config.config,
324
+ dependencyGraph: {
325
+ packages: Array.from(this.graph.packages.values()).map(pkg => ({
326
+ name: pkg.name,
327
+ version: pkg.version,
328
+ path: pkg.path,
329
+ dependencies: Array.from(pkg.dependencies)
330
+ })),
331
+ edges: Array.from(this.graph.edges.entries()).map(([pkg, deps]) => [
332
+ pkg,
333
+ Array.from(deps)
334
+ ])
335
+ },
336
+ buildOrder: [
337
+ ...this.state.pending,
338
+ ...this.state.ready,
339
+ ...this.state.running.map(r => r.name),
340
+ ...this.state.completed,
341
+ ...this.state.failed.map(f => f.name),
342
+ ...this.state.skipped
343
+ ],
344
+ executionMode: 'parallel',
345
+ maxConcurrency: this.config.maxConcurrency,
346
+ state: this.state,
347
+ publishedVersions: this.publishedVersions.map(pv => ({
348
+ packageName: pv.name,
349
+ version: pv.version,
350
+ publishTime: pv.time.toISOString()
351
+ })),
352
+ retryAttempts: Object.fromEntries(this.retryAttempts),
353
+ lastRetryTime: {},
354
+ packageStartTimes: Object.fromEntries(Array.from(this.packageStartTimes.entries()).map(([k, v]) => [k, v.toISOString()])),
355
+ packageEndTimes: Object.fromEntries(Array.from(this.packageEndTimes.entries()).map(([k, v]) => [k, v.toISOString()])),
356
+ packageDurations: Object.fromEntries(this.packageDurations),
357
+ totalStartTime: this.startTime.toISOString(),
358
+ recoveryHints: [],
359
+ canRecover: true
360
+ };
361
+ await this.checkpointManager.save(checkpoint);
362
+ this.emit('checkpoint:saved', { timestamp: new Date() });
363
+ }
364
+ /**
365
+ * Load checkpoint
366
+ */
367
+ async loadCheckpoint() {
368
+ const checkpoint = await this.checkpointManager.load();
369
+ if (!checkpoint) {
370
+ this.logger.warn('CHECKPOINT_NOT_FOUND: No checkpoint file found | Action: Starting fresh execution | Path: ' + this.config.checkpointPath);
371
+ return;
372
+ }
373
+ this.logger.info('CHECKPOINT_LOADING: Loading execution checkpoint | Purpose: Resume previous execution | Path: ' + this.config.checkpointPath);
374
+ this.logger.info(`CHECKPOINT_EXECUTION_ID: Checkpoint execution identifier | ID: ${checkpoint.executionId}`);
375
+ this.logger.info(`CHECKPOINT_STATE_COMPLETED: Completed packages from checkpoint | Count: ${checkpoint.state.completed.length} packages`);
376
+ this.logger.info(`CHECKPOINT_STATE_FAILED: Failed packages from checkpoint | Count: ${checkpoint.state.failed.length} packages`);
377
+ // Restore state
378
+ this.executionId = checkpoint.executionId;
379
+ this.startTime = new Date(checkpoint.totalStartTime);
380
+ this.state = checkpoint.state;
381
+ // Restore timing data
382
+ for (const [pkg, time] of Object.entries(checkpoint.packageStartTimes)) {
383
+ this.packageStartTimes.set(pkg, new Date(time));
384
+ }
385
+ for (const [pkg, time] of Object.entries(checkpoint.packageEndTimes)) {
386
+ this.packageEndTimes.set(pkg, new Date(time));
387
+ }
388
+ for (const [pkg, duration] of Object.entries(checkpoint.packageDurations)) {
389
+ this.packageDurations.set(pkg, duration);
390
+ }
391
+ // Restore retry attempts
392
+ for (const [pkg, attempts] of Object.entries(checkpoint.retryAttempts)) {
393
+ this.retryAttempts.set(pkg, attempts);
394
+ }
395
+ // Clear running state (cannot resume mid-execution)
396
+ for (const running of this.state.running) {
397
+ this.state.pending.push(running.name);
398
+ }
399
+ this.state.running = [];
400
+ // CRITICAL FIX: Re-evaluate skipped packages
401
+ // After loading checkpoint (especially with --mark-completed), packages that were
402
+ // skipped due to failed dependencies might now be eligible to run if those
403
+ // dependencies are now completed. Move them back to pending for reassessment.
404
+ const unblocked = [];
405
+ for (const packageName of this.state.skipped) {
406
+ // Check if all dependencies are now completed
407
+ const dependencies = this.graph.edges.get(packageName) || new Set();
408
+ const allDepsCompleted = Array.from(dependencies).every(dep => this.state.completed.includes(dep) || this.state.skippedNoChanges.includes(dep));
409
+ // Check if any dependencies are still failed
410
+ const anyDepsFailed = Array.from(dependencies).some(dep => this.state.failed.some(f => f.name === dep));
411
+ if (allDepsCompleted && !anyDepsFailed) {
412
+ unblocked.push(packageName);
413
+ }
414
+ }
415
+ // Move unblocked packages back to pending
416
+ if (unblocked.length > 0) {
417
+ this.logger.info(`PACKAGES_UNBLOCKED: Dependencies satisfied, packages now ready | Count: ${unblocked.length} | Packages: ${unblocked.join(', ')} | Status: ready-to-execute`);
418
+ for (const packageName of unblocked) {
419
+ this.state.skipped = this.state.skipped.filter(p => p !== packageName);
420
+ this.state.pending.push(packageName);
421
+ }
422
+ }
423
+ }
424
+ /**
425
+ * Build execution result
426
+ */
427
+ buildExecutionResult() {
428
+ const totalDuration = Date.now() - this.startTime.getTime();
429
+ const completedDurations = Array.from(this.packageDurations.values());
430
+ const averageDuration = completedDurations.length > 0
431
+ ? completedDurations.reduce((a, b) => a + b, 0) / completedDurations.length
432
+ : 0;
433
+ const metrics = {
434
+ totalDuration,
435
+ averagePackageDuration: averageDuration,
436
+ peakConcurrency: this.resourceMonitor.getMetrics().peakConcurrency,
437
+ averageConcurrency: this.resourceMonitor.getMetrics().averageConcurrency
438
+ };
439
+ return {
440
+ success: this.state.failed.length === 0,
441
+ totalPackages: this.graph.packages.size,
442
+ completed: this.state.completed,
443
+ failed: this.state.failed,
444
+ skipped: this.state.skipped,
445
+ skippedNoChanges: this.state.skippedNoChanges,
446
+ metrics
447
+ };
448
+ }
449
+ /**
450
+ * Check if error is retriable
451
+ */
452
+ isRetriableError(error) {
453
+ const errorText = error.message || String(error);
454
+ const stackText = error.stack || '';
455
+ const fullText = `${errorText}\n${stackText}`;
456
+ const retriablePatterns = [
457
+ // Network errors
458
+ /ETIMEDOUT/i,
459
+ /ECONNRESET/i,
460
+ /ENOTFOUND/i,
461
+ /ECONNREFUSED/i,
462
+ /rate limit/i,
463
+ /temporary failure/i,
464
+ /try again/i,
465
+ /gateway timeout/i,
466
+ /service unavailable/i,
467
+ // Git lock file errors (common in parallel execution)
468
+ /index\.lock/i,
469
+ /\.git\/index\.lock/i,
470
+ /unable to create.*lock/i,
471
+ /lock file.*exists/i,
472
+ // npm install race conditions
473
+ /ENOENT.*npm-cache/i,
474
+ /EBUSY.*npm/i,
475
+ /npm.*EEXIST/i,
476
+ // GitHub API temporary errors
477
+ /abuse detection/i,
478
+ /secondary rate limit/i,
479
+ /GitHub API.*unavailable/i,
480
+ // Timeout errors (might be transient)
481
+ /timeout waiting for/i,
482
+ /timed out after/i
483
+ ];
484
+ const isRetriable = retriablePatterns.some(pattern => pattern.test(fullText));
485
+ // Non-retriable errors that should fail immediately
486
+ const nonRetriablePatterns = [
487
+ /test.*failed/i,
488
+ /coverage.*below.*threshold/i,
489
+ /compilation.*failed/i,
490
+ /build.*failed/i,
491
+ /merge.*conflict/i,
492
+ /uncommitted changes/i,
493
+ /working.*dirty/i,
494
+ /authentication.*failed/i,
495
+ /permission denied/i
496
+ ];
497
+ const isNonRetriable = nonRetriablePatterns.some(pattern => pattern.test(fullText));
498
+ // If explicitly non-retriable, don't retry
499
+ if (isNonRetriable) {
500
+ return false;
501
+ }
502
+ return isRetriable;
503
+ }
504
+ /**
505
+ * Calculate retry delay with exponential backoff
506
+ */
507
+ calculateRetryDelay(attemptNumber) {
508
+ const initialDelay = this.config.initialRetryDelay || 5000;
509
+ const maxDelay = this.config.maxRetryDelay || 60000;
510
+ const multiplier = this.config.backoffMultiplier || 2;
511
+ const delay = Math.min(initialDelay * Math.pow(multiplier, attemptNumber - 1), maxDelay);
512
+ // Add jitter
513
+ const jitter = Math.random() * 0.1 * delay;
514
+ return delay + jitter;
515
+ }
516
+ /**
517
+ * Format duration in human-readable format
518
+ */
519
+ formatDuration(ms) {
520
+ const seconds = Math.floor(ms / 1000);
521
+ const minutes = Math.floor(seconds / 60);
522
+ if (minutes > 0) {
523
+ return `${minutes}m ${seconds % 60}s`;
524
+ }
525
+ return `${seconds}s`;
526
+ }
527
+ /**
528
+ * Extract detailed error information from error message and stack
529
+ */
530
+ extractErrorDetails(error, packageName) {
531
+ const errorMsg = error.message || '';
532
+ const errorStack = error.stack || '';
533
+ const fullText = `${errorMsg}\n${errorStack}`;
534
+ // Get log file path from error if attached, otherwise use default
535
+ const logFile = error.logFilePath || this.getLogFilePath(packageName);
536
+ // Test coverage failure
537
+ if (fullText.match(/coverage.*below.*threshold|coverage.*insufficient/i)) {
538
+ const coverageMatch = fullText.match(/(\w+):\s*(\d+\.?\d*)%.*threshold:\s*(\d+\.?\d*)%/i);
539
+ return {
540
+ type: 'test_coverage',
541
+ context: coverageMatch
542
+ ? `${coverageMatch[1]}: ${coverageMatch[2]}% (threshold: ${coverageMatch[3]}%)`
543
+ : 'Coverage below threshold',
544
+ logFile,
545
+ suggestion: `cd ${this.getPackagePath(packageName)} && npm test -- --coverage`
546
+ };
547
+ }
548
+ // Build/compile errors
549
+ if (fullText.match(/compilation.*failed|build.*failed|tsc.*error/i)) {
550
+ return {
551
+ type: 'build_error',
552
+ context: this.extractFirstErrorLine(fullText),
553
+ logFile,
554
+ suggestion: `cd ${this.getPackagePath(packageName)} && npm run build`
555
+ };
556
+ }
557
+ // Merge conflicts
558
+ if (fullText.match(/merge.*conflict|conflict.*marker|<<<<<<<|>>>>>>>/i)) {
559
+ return {
560
+ type: 'merge_conflict',
561
+ context: 'Unresolved merge conflicts detected',
562
+ logFile,
563
+ suggestion: `cd ${this.getPackagePath(packageName)} && git status`
564
+ };
565
+ }
566
+ // Test failures
567
+ if (fullText.match(/test.*failed|tests.*failed|\d+\s+failing/i)) {
568
+ const failMatch = fullText.match(/(\d+)\s+failing/i);
569
+ return {
570
+ type: 'test_failure',
571
+ context: failMatch ? `${failMatch[1]} test(s) failing` : 'Tests failed',
572
+ logFile,
573
+ suggestion: `cd ${this.getPackagePath(packageName)} && npm test`
574
+ };
575
+ }
576
+ // Timeout errors
577
+ if (fullText.match(/timeout|timed.*out/i)) {
578
+ return {
579
+ type: 'timeout',
580
+ context: this.extractFirstErrorLine(fullText),
581
+ logFile,
582
+ suggestion: 'Consider increasing timeout or checking for stuck processes'
583
+ };
584
+ }
585
+ // PR/Git errors
586
+ if (fullText.match(/pull request|pr|github/i) && fullText.match(/not mergeable|conflict/i)) {
587
+ return {
588
+ type: 'pr_conflict',
589
+ context: 'Pull request has merge conflicts',
590
+ logFile,
591
+ suggestion: 'Resolve conflicts in the PR and re-run with --continue'
592
+ };
593
+ }
594
+ // Git state errors
595
+ if (fullText.match(/uncommitted changes|working.*dirty|not.*clean/i)) {
596
+ return {
597
+ type: 'git_state',
598
+ context: 'Working directory has uncommitted changes',
599
+ logFile,
600
+ suggestion: `cd ${this.getPackagePath(packageName)} && git status`
601
+ };
602
+ }
603
+ // npm install / dependency errors
604
+ if (fullText.match(/npm.*install|ERESOLVE|Cannot find module/i)) {
605
+ return {
606
+ type: 'dependency_error',
607
+ context: this.extractFirstErrorLine(fullText),
608
+ logFile,
609
+ suggestion: `cd ${this.getPackagePath(packageName)} && rm -rf node_modules package-lock.json && npm install`
610
+ };
611
+ }
612
+ // Git lock file errors
613
+ if (fullText.match(/index\.lock|\.git\/index\.lock|unable to create.*lock/i)) {
614
+ return {
615
+ type: 'git_lock',
616
+ context: 'Git lock file conflict - another git process running',
617
+ logFile,
618
+ suggestion: `cd ${this.getPackagePath(packageName)} && rm -f .git/index.lock`
619
+ };
620
+ }
621
+ // No changes detected (not really an error, but handle it)
622
+ if (fullText.match(/no.*changes|already.*published|nothing.*to.*publish/i)) {
623
+ return {
624
+ type: 'no_changes',
625
+ context: 'No changes detected - package already published',
626
+ logFile,
627
+ suggestion: 'This is expected if package was previously published'
628
+ };
629
+ }
630
+ // Generic error with log file
631
+ return {
632
+ type: 'unknown',
633
+ context: errorMsg.split('\n')[0].substring(0, 200),
634
+ logFile
635
+ };
636
+ }
637
+ extractFirstErrorLine(text) {
638
+ const lines = text.split('\n');
639
+ for (const line of lines) {
640
+ if (line.match(/error|failed|exception/i) && line.trim().length > 10) {
641
+ return line.trim().substring(0, 200);
642
+ }
643
+ }
644
+ return text.split('\n')[0].substring(0, 200);
645
+ }
646
+ getPackagePath(packageName) {
647
+ const pkgInfo = this.graph.packages.get(packageName);
648
+ return pkgInfo?.path || '.';
649
+ }
650
+ getLogFilePath(packageName) {
651
+ const pkgPath = this.getPackagePath(packageName);
652
+ const outputDir = this.config.config.outputDirectory || 'output/kodrdriv';
653
+ // Return wildcard pattern as fallback (log file should be attached to error directly)
654
+ // This is used as a fallback when log file path isn't attached to the error
655
+ return `${pkgPath}/${outputDir}/publish_*.log`;
656
+ }
657
+ }
658
+ //# sourceMappingURL=DynamicTaskPool.js.map