aether-colony 3.1.5 → 3.1.16

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 (133) hide show
  1. package/.claude/commands/ant/archaeology.md +12 -0
  2. package/.claude/commands/ant/build.md +382 -319
  3. package/.claude/commands/ant/chaos.md +23 -1
  4. package/.claude/commands/ant/colonize.md +147 -87
  5. package/.claude/commands/ant/continue.md +213 -23
  6. package/.claude/commands/ant/council.md +22 -0
  7. package/.claude/commands/ant/dream.md +18 -0
  8. package/.claude/commands/ant/entomb.md +178 -6
  9. package/.claude/commands/ant/init.md +87 -13
  10. package/.claude/commands/ant/lay-eggs.md +45 -5
  11. package/.claude/commands/ant/oracle.md +82 -9
  12. package/.claude/commands/ant/organize.md +2 -2
  13. package/.claude/commands/ant/pause-colony.md +86 -28
  14. package/.claude/commands/ant/phase.md +26 -0
  15. package/.claude/commands/ant/plan.md +204 -111
  16. package/.claude/commands/ant/resume-colony.md +23 -1
  17. package/.claude/commands/ant/resume.md +159 -0
  18. package/.claude/commands/ant/seal.md +177 -3
  19. package/.claude/commands/ant/swarm.md +78 -97
  20. package/.claude/commands/ant/verify-castes.md +7 -7
  21. package/.claude/commands/ant/watch.md +17 -0
  22. package/.opencode/agents/aether-ambassador.md +97 -0
  23. package/.opencode/agents/aether-archaeologist.md +91 -0
  24. package/.opencode/agents/aether-architect.md +66 -0
  25. package/.opencode/agents/aether-auditor.md +111 -0
  26. package/.opencode/agents/aether-builder.md +28 -10
  27. package/.opencode/agents/aether-chaos.md +98 -0
  28. package/.opencode/agents/aether-chronicler.md +80 -0
  29. package/.opencode/agents/aether-gatekeeper.md +107 -0
  30. package/.opencode/agents/aether-guardian.md +107 -0
  31. package/.opencode/agents/aether-includer.md +108 -0
  32. package/.opencode/agents/aether-keeper.md +106 -0
  33. package/.opencode/agents/aether-measurer.md +119 -0
  34. package/.opencode/agents/aether-probe.md +91 -0
  35. package/.opencode/agents/aether-queen.md +72 -19
  36. package/.opencode/agents/aether-route-setter.md +85 -0
  37. package/.opencode/agents/aether-sage.md +98 -0
  38. package/.opencode/agents/aether-scout.md +33 -15
  39. package/.opencode/agents/aether-surveyor-disciplines.md +334 -0
  40. package/.opencode/agents/aether-surveyor-nest.md +272 -0
  41. package/.opencode/agents/aether-surveyor-pathogens.md +209 -0
  42. package/.opencode/agents/aether-surveyor-provisions.md +277 -0
  43. package/.opencode/agents/aether-tracker.md +91 -0
  44. package/.opencode/agents/aether-watcher.md +30 -12
  45. package/.opencode/agents/aether-weaver.md +87 -0
  46. package/.opencode/agents/workers.md +1034 -0
  47. package/.opencode/commands/ant/archaeology.md +44 -26
  48. package/.opencode/commands/ant/build.md +326 -294
  49. package/.opencode/commands/ant/chaos.md +32 -4
  50. package/.opencode/commands/ant/colonize.md +119 -93
  51. package/.opencode/commands/ant/continue.md +98 -10
  52. package/.opencode/commands/ant/council.md +28 -0
  53. package/.opencode/commands/ant/dream.md +24 -0
  54. package/.opencode/commands/ant/entomb.md +73 -1
  55. package/.opencode/commands/ant/feedback.md +8 -2
  56. package/.opencode/commands/ant/flag.md +9 -3
  57. package/.opencode/commands/ant/flags.md +8 -2
  58. package/.opencode/commands/ant/focus.md +8 -2
  59. package/.opencode/commands/ant/help.md +12 -0
  60. package/.opencode/commands/ant/init.md +49 -4
  61. package/.opencode/commands/ant/lay-eggs.md +30 -2
  62. package/.opencode/commands/ant/oracle.md +39 -7
  63. package/.opencode/commands/ant/organize.md +8 -2
  64. package/.opencode/commands/ant/pause-colony.md +54 -1
  65. package/.opencode/commands/ant/phase.md +36 -4
  66. package/.opencode/commands/ant/plan.md +224 -116
  67. package/.opencode/commands/ant/redirect.md +8 -2
  68. package/.opencode/commands/ant/resume-colony.md +51 -26
  69. package/.opencode/commands/ant/seal.md +76 -0
  70. package/.opencode/commands/ant/status.md +50 -20
  71. package/.opencode/commands/ant/swarm.md +108 -104
  72. package/.opencode/commands/ant/tunnels.md +107 -2
  73. package/CHANGELOG.md +16 -0
  74. package/README.md +199 -86
  75. package/bin/cli.js +142 -25
  76. package/bin/generate-commands.sh +100 -16
  77. package/bin/lib/caste-colors.js +5 -5
  78. package/bin/lib/errors.js +16 -0
  79. package/bin/lib/file-lock.js +279 -44
  80. package/bin/lib/state-sync.js +206 -23
  81. package/bin/lib/update-transaction.js +206 -24
  82. package/bin/sync-to-runtime.sh +138 -0
  83. package/package.json +2 -2
  84. package/runtime/CONTEXT.md +160 -0
  85. package/runtime/aether-utils.sh +1421 -55
  86. package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +1343 -0
  87. package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +2642 -0
  88. package/runtime/docs/PHEROMONE-INJECTION.md +240 -0
  89. package/runtime/docs/PHEROMONE-INTEGRATION.md +192 -0
  90. package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +426 -0
  91. package/runtime/docs/README.md +94 -0
  92. package/runtime/docs/VISUAL-OUTPUT-SPEC.md +219 -0
  93. package/runtime/docs/biological-reference.md +272 -0
  94. package/runtime/docs/codebase-review.md +399 -0
  95. package/runtime/docs/command-sync.md +164 -0
  96. package/runtime/docs/implementation-learnings.md +89 -0
  97. package/runtime/docs/known-issues.md +217 -0
  98. package/runtime/docs/namespace.md +148 -0
  99. package/runtime/docs/planning-discipline.md +159 -0
  100. package/runtime/exchange/pheromone-xml.sh +574 -0
  101. package/runtime/exchange/registry-xml.sh +269 -0
  102. package/runtime/exchange/wisdom-xml.sh +312 -0
  103. package/runtime/lib/queen-utils.sh +729 -0
  104. package/runtime/model-profiles.yaml +100 -0
  105. package/runtime/recover.sh +136 -0
  106. package/runtime/schemas/aether-types.xsd +255 -0
  107. package/runtime/schemas/colony-registry.xsd +309 -0
  108. package/runtime/schemas/pheromone.xsd +163 -0
  109. package/runtime/schemas/prompt.xsd +416 -0
  110. package/runtime/schemas/queen-wisdom.xsd +325 -0
  111. package/runtime/schemas/worker-priming.xsd +276 -0
  112. package/runtime/templates/QUEEN.md.template +79 -0
  113. package/runtime/utils/atomic-write.sh +5 -5
  114. package/runtime/utils/chamber-utils.sh +6 -3
  115. package/runtime/utils/error-handler.sh +200 -0
  116. package/runtime/utils/queen-to-md.xsl +395 -0
  117. package/runtime/utils/spawn-tree.sh +428 -0
  118. package/runtime/utils/spawn-with-model.sh +56 -0
  119. package/runtime/utils/state-loader.sh +215 -0
  120. package/runtime/utils/swarm-display.sh +5 -5
  121. package/runtime/utils/watch-spawn-tree.sh +90 -22
  122. package/runtime/utils/xml-compose.sh +247 -0
  123. package/runtime/utils/xml-core.sh +186 -0
  124. package/runtime/utils/xml-utils.sh +2196 -0
  125. package/runtime/verification-loop.md +1 -1
  126. package/runtime/workers-new-castes.md +516 -0
  127. package/runtime/workers.md +18 -6
  128. package/.aether/visualizations/anthill-stages/brood-stable.txt +0 -26
  129. package/.aether/visualizations/anthill-stages/crowned-anthill.txt +0 -30
  130. package/.aether/visualizations/anthill-stages/first-mound.txt +0 -18
  131. package/.aether/visualizations/anthill-stages/open-chambers.txt +0 -24
  132. package/.aether/visualizations/anthill-stages/sealed-chambers.txt +0 -28
  133. package/.aether/visualizations/anthill-stages/ventilated-nest.txt +0 -27
@@ -10,7 +10,7 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
- const { FileSystemError } = require('./errors');
13
+ const { FileSystemError, ConfigurationError } = require('./errors');
14
14
 
15
15
  /**
16
16
  * Default configuration for lock behavior
@@ -20,8 +20,15 @@ const DEFAULT_OPTIONS = {
20
20
  timeout: 5000, // 5 seconds total timeout
21
21
  retryInterval: 50, // 50ms between retries
22
22
  maxRetries: 100, // Total 5 seconds max wait
23
+ maxLockAge: 5 * 60 * 1000, // 5 minutes - configurable stale lock threshold
23
24
  };
24
25
 
26
+ /**
27
+ * Module-level set of registered cleanup functions (PLAN-006 fix #4)
28
+ * Prevents duplicate cleanup handler registration across multiple FileLock instances
29
+ */
30
+ const registeredCleanups = new Set();
31
+
25
32
  /**
26
33
  * FileLock class for exclusive file locking with stale detection
27
34
  *
@@ -37,9 +44,51 @@ class FileLock {
37
44
  * @param {number} options.timeout - Total timeout in milliseconds (default: 5000)
38
45
  * @param {number} options.retryInterval - Milliseconds between retries (default: 50)
39
46
  * @param {number} options.maxRetries - Maximum retry attempts (default: 100)
47
+ * @param {number} options.maxLockAge - Maximum lock age in ms before considered stale (default: 300000)
40
48
  */
41
49
  constructor(options = {}) {
42
50
  this.options = { ...DEFAULT_OPTIONS, ...options };
51
+
52
+ // Validate lockDir (PLAN-006 fix #2)
53
+ if (!this.options.lockDir || typeof this.options.lockDir !== 'string') {
54
+ throw new ConfigurationError(
55
+ 'lockDir must be a non-empty string',
56
+ { lockDir: this.options.lockDir }
57
+ );
58
+ }
59
+
60
+ // Validate timeout (PLAN-006 fix #5)
61
+ if (typeof this.options.timeout !== 'number' || this.options.timeout < 0) {
62
+ throw new ConfigurationError(
63
+ 'timeout must be a non-negative number',
64
+ { timeout: this.options.timeout }
65
+ );
66
+ }
67
+
68
+ // Validate retryInterval
69
+ if (typeof this.options.retryInterval !== 'number' || this.options.retryInterval < 0) {
70
+ throw new ConfigurationError(
71
+ 'retryInterval must be a non-negative number',
72
+ { retryInterval: this.options.retryInterval }
73
+ );
74
+ }
75
+
76
+ // Validate maxRetries
77
+ if (typeof this.options.maxRetries !== 'number' || this.options.maxRetries < 0) {
78
+ throw new ConfigurationError(
79
+ 'maxRetries must be a non-negative number',
80
+ { maxRetries: this.options.maxRetries }
81
+ );
82
+ }
83
+
84
+ // Validate maxLockAge (PLAN-007 Fix 1)
85
+ if (typeof this.options.maxLockAge !== 'number' || this.options.maxLockAge < 0) {
86
+ throw new ConfigurationError(
87
+ 'maxLockAge must be a non-negative number',
88
+ { maxLockAge: this.options.maxLockAge }
89
+ );
90
+ }
91
+
43
92
  this.currentLock = null;
44
93
  this.currentPidFile = null;
45
94
 
@@ -69,9 +118,20 @@ class FileLock {
69
118
 
70
119
  /**
71
120
  * Register process cleanup handlers to ensure locks are released on exit
121
+ * Uses module-level tracking to prevent duplicate registrations (PLAN-006 fix #4)
72
122
  * @private
73
123
  */
74
124
  _registerCleanupHandlers() {
125
+ // Create unique identifier for this cleanup based on lock directory
126
+ const cleanupId = `filelock-${this.options.lockDir}`;
127
+
128
+ // Only register if not already registered
129
+ if (registeredCleanups.has(cleanupId)) {
130
+ return;
131
+ }
132
+
133
+ registeredCleanups.add(cleanupId);
134
+
75
135
  const cleanup = () => {
76
136
  this.release();
77
137
  };
@@ -131,50 +191,111 @@ class FileLock {
131
191
  }
132
192
  }
133
193
 
194
+ /**
195
+ * Safely unlink a file, ignoring ENOENT errors
196
+ *
197
+ * @param {string} filePath - Path to file to unlink
198
+ * @private
199
+ */
200
+ _safeUnlink(filePath) {
201
+ try {
202
+ fs.unlinkSync(filePath);
203
+ } catch (error) {
204
+ if (error.code !== 'ENOENT') {
205
+ // Log but don't throw - we're cleaning up
206
+ console.warn(`Warning: Failed to clean up ${filePath}: ${error.message}`);
207
+ }
208
+ }
209
+ }
210
+
134
211
  /**
135
212
  * Clean up stale lock files
136
213
  *
214
+ * Checks both PID file and lock file for PID (handles crash scenarios
215
+ * where only one file was created). Also checks lock age to handle
216
+ * PID reuse race condition.
217
+ *
137
218
  * @param {string} lockFile - Path to lock file
138
219
  * @param {string} pidFile - Path to PID file
220
+ * @returns {boolean} True if lock was cleaned up, false if still held
139
221
  * @private
140
222
  */
141
223
  _cleanupStaleLock(lockFile, pidFile) {
224
+ // Maximum lock age before considering stale (configurable, default 5 minutes)
225
+ // This handles PID reuse race condition
226
+ const maxLockAgeMs = this.options.maxLockAge;
227
+
142
228
  try {
143
- // Try to read the PID from the pid file
144
- if (fs.existsSync(pidFile)) {
145
- const pidData = fs.readFileSync(pidFile, 'utf8').trim();
146
- const pid = parseInt(pidData, 10);
147
-
148
- if (!isNaN(pid)) {
149
- // Check if the process is still running
150
- if (this._isProcessRunning(pid)) {
151
- // Process is running, lock is not stale
152
- return false;
229
+ // Check lock file age first (handles PID reuse)
230
+ if (fs.existsSync(lockFile)) {
231
+ try {
232
+ const stat = fs.statSync(lockFile);
233
+ const lockAge = Date.now() - stat.mtimeMs;
234
+
235
+ if (lockAge > maxLockAgeMs) {
236
+ // Lock is old enough to be considered stale regardless of PID
237
+ this._safeUnlink(lockFile);
238
+ this._safeUnlink(pidFile);
239
+ return true;
153
240
  }
241
+ } catch {
242
+ // Cannot stat, proceed with PID check
154
243
  }
155
244
  }
156
245
 
157
- // Either no valid PID or process not running - clean up stale lock
158
- try {
159
- fs.unlinkSync(lockFile);
160
- } catch (error) {
161
- if (error.code !== 'ENOENT') {
162
- throw error;
246
+ let pid = null;
247
+
248
+ // Try to read PID from PID file first
249
+ if (fs.existsSync(pidFile)) {
250
+ try {
251
+ const pidData = fs.readFileSync(pidFile, 'utf8').trim();
252
+
253
+ // Validate PID is a positive integer (PLAN-006 fix #3)
254
+ if (!/^\d+$/.test(pidData)) {
255
+ // PID file contains invalid data - clean it up
256
+ this._safeUnlink(lockFile);
257
+ this._safeUnlink(pidFile);
258
+ return true;
259
+ }
260
+
261
+ pid = parseInt(pidData, 10);
262
+ } catch (readError) {
263
+ // PID file unreadable - will clean up
163
264
  }
164
265
  }
165
266
 
166
- try {
167
- fs.unlinkSync(pidFile);
168
- } catch (error) {
169
- if (error.code !== 'ENOENT') {
170
- throw error;
267
+ // If no valid PID from PID file, try lock file itself
268
+ if ((pid === null || isNaN(pid)) && fs.existsSync(lockFile)) {
269
+ try {
270
+ const lockData = fs.readFileSync(lockFile, 'utf8').trim();
271
+
272
+ // Validate PID is a positive integer
273
+ if (!/^\d+$/.test(lockData)) {
274
+ // Lock file contains invalid data - clean it up
275
+ this._safeUnlink(lockFile);
276
+ this._safeUnlink(pidFile);
277
+ return true;
278
+ }
279
+
280
+ pid = parseInt(lockData, 10);
281
+ } catch (readError) {
282
+ // Lock file unreadable - will clean up
171
283
  }
172
284
  }
173
285
 
286
+ // Check if process is running
287
+ if (!isNaN(pid) && this._isProcessRunning(pid)) {
288
+ // Process is running, lock is valid
289
+ return false;
290
+ }
291
+
292
+ // Either no valid PID or process not running - clean up stale lock
293
+ this._safeUnlink(lockFile);
294
+ this._safeUnlink(pidFile);
295
+
174
296
  return true;
175
297
  } catch (error) {
176
298
  if (error.code === 'ENOENT') {
177
- // Lock file doesn't exist, consider it cleaned
178
299
  return true;
179
300
  }
180
301
  throw new FileSystemError(
@@ -187,39 +308,73 @@ class FileLock {
187
308
  /**
188
309
  * Attempt to acquire a lock atomically
189
310
  *
311
+ * Uses PID-file-first ordering for crash recovery:
312
+ * 1. Write PID file first (if this fails, no lock file created)
313
+ * 2. Create lock file atomically
314
+ * 3. On failure, clean up both files
315
+ *
190
316
  * @param {string} lockFile - Path to lock file
191
317
  * @param {string} pidFile - Path to PID file
192
318
  * @returns {boolean} True if lock acquired, false otherwise
319
+ * @throws {FileSystemError} On unexpected filesystem errors
193
320
  * @private
194
321
  */
195
322
  _tryAcquire(lockFile, pidFile) {
196
- try {
197
- // Attempt atomic creation using 'wx' flag (fails if file exists)
198
- const fd = fs.openSync(lockFile, 'wx');
323
+ let pidFileCreated = false;
324
+ let lockFileCreated = false;
199
325
 
326
+ try {
327
+ // Step 1: Write PID file first (easier to clean up if lock fails)
200
328
  try {
201
- // Write current PID to lock file
202
- const pid = process.pid;
203
- fs.writeFileSync(fd, pid.toString(), 'utf8');
204
- } finally {
205
- fs.closeSync(fd);
329
+ fs.writeFileSync(pidFile, process.pid.toString(), 'utf8');
330
+ pidFileCreated = true;
331
+ } catch (pidError) {
332
+ // Cannot write PID file - cannot proceed
333
+ throw new FileSystemError(
334
+ `Failed to write PID file: ${pidFile}`,
335
+ { error: pidError.message, code: pidError.code }
336
+ );
206
337
  }
207
338
 
208
- // Also write to separate PID file for easy reading
209
- fs.writeFileSync(pidFile, process.pid.toString(), 'utf8');
339
+ // Step 2: Create lock file atomically
340
+ try {
341
+ const fd = fs.openSync(lockFile, 'wx');
342
+ lockFileCreated = true;
343
+
344
+ try {
345
+ // Write PID to lock file as well (for redundancy)
346
+ fs.writeFileSync(fd, process.pid.toString(), 'utf8');
347
+ } finally {
348
+ fs.closeSync(fd);
349
+ }
350
+ } catch (lockError) {
351
+ if (lockError.code === 'EEXIST') {
352
+ // Lock file exists - clean up our PID file
353
+ this._safeUnlink(pidFile);
354
+ return false;
355
+ }
356
+ throw lockError;
357
+ }
210
358
 
211
- // Track current lock
359
+ // Step 3: Track current lock (only after both files created)
212
360
  this.currentLock = lockFile;
213
361
  this.currentPidFile = pidFile;
214
362
 
215
363
  return true;
364
+
216
365
  } catch (error) {
217
- if (error.code === 'EEXIST') {
218
- // Lock file already exists
219
- return false;
366
+ // Clean up on any failure
367
+ if (lockFileCreated) {
368
+ this._safeUnlink(lockFile);
369
+ }
370
+ if (pidFileCreated) {
371
+ this._safeUnlink(pidFile);
372
+ }
373
+
374
+ if (error instanceof FileSystemError) {
375
+ throw error;
220
376
  }
221
377
 
222
- // Unexpected error
223
378
  throw new FileSystemError(
224
379
  `Failed to acquire lock: ${lockFile}`,
225
380
  { error: error.message, code: error.code }
@@ -228,7 +383,10 @@ class FileLock {
228
383
  }
229
384
 
230
385
  /**
231
- * Acquire an exclusive lock on a file
386
+ * Acquire an exclusive lock on a file (SYNCHRONOUS - may block event loop)
387
+ *
388
+ * WARNING: This method uses a busy-wait loop that blocks the Node.js event loop
389
+ * during retry intervals. For non-blocking operation, use acquireAsync() instead.
232
390
  *
233
391
  * @param {string} filePath - Path to the file to lock
234
392
  * @returns {boolean} True if lock acquired, false on timeout
@@ -281,25 +439,26 @@ class FileLock {
281
439
  *
282
440
  * This method is idempotent - safe to call multiple times.
283
441
  *
284
- * @returns {boolean} True if a lock was released, false if no lock was held
442
+ * @returns {boolean} True if lock was fully released, false if no lock was held
443
+ * or if deletion failed (check console.warn for details)
285
444
  */
286
445
  release() {
287
446
  if (!this.currentLock) {
288
447
  return false;
289
448
  }
290
449
 
291
- let released = false;
450
+ let success = true;
292
451
 
293
452
  // Delete lock file
294
453
  try {
295
454
  if (fs.existsSync(this.currentLock)) {
296
455
  fs.unlinkSync(this.currentLock);
297
- released = true;
298
456
  }
299
457
  } catch (error) {
300
458
  if (error.code !== 'ENOENT') {
301
459
  // Log but don't throw - we're cleaning up
302
460
  console.warn(`Warning: Failed to remove lock file: ${error.message}`);
461
+ success = false;
303
462
  }
304
463
  }
305
464
 
@@ -312,6 +471,7 @@ class FileLock {
312
471
  } catch (error) {
313
472
  if (error.code !== 'ENOENT') {
314
473
  console.warn(`Warning: Failed to remove PID file: ${error.message}`);
474
+ success = false;
315
475
  }
316
476
  }
317
477
  }
@@ -320,7 +480,7 @@ class FileLock {
320
480
  this.currentLock = null;
321
481
  this.currentPidFile = null;
322
482
 
323
- return released;
483
+ return success;
324
484
  }
325
485
 
326
486
  /**
@@ -358,7 +518,10 @@ class FileLock {
358
518
  }
359
519
 
360
520
  /**
361
- * Wait for a lock to be released
521
+ * Wait for a lock to be released (SYNCHRONOUS - may block event loop)
522
+ *
523
+ * WARNING: This method uses a busy-wait loop that blocks the Node.js event loop.
524
+ * For non-blocking operation, use waitForLockAsync() instead.
362
525
  *
363
526
  * @param {string} filePath - Path to wait for
364
527
  * @param {number} maxWait - Maximum milliseconds to wait (default: timeout option)
@@ -383,6 +546,78 @@ class FileLock {
383
546
  return true;
384
547
  }
385
548
 
549
+ /**
550
+ * Acquire an exclusive lock asynchronously (yields to event loop)
551
+ *
552
+ * This is the non-blocking version of acquire(). It uses setTimeout for
553
+ * delays, allowing other async operations to run during wait periods.
554
+ *
555
+ * @param {string} filePath - Path to the file to lock
556
+ * @returns {Promise<boolean>} True if lock acquired, false on timeout
557
+ * @throws {FileSystemError} On unexpected filesystem errors
558
+ */
559
+ async acquireAsync(filePath) {
560
+ const { lockFile, pidFile } = this._getLockPaths(filePath);
561
+
562
+ // Check for existing lock and handle stale locks
563
+ if (fs.existsSync(lockFile)) {
564
+ this._cleanupStaleLock(lockFile, pidFile);
565
+ // Lock is held by running process if not cleaned
566
+ }
567
+
568
+ // Try to acquire lock with retries
569
+ let retryCount = 0;
570
+ const startTime = Date.now();
571
+
572
+ while (retryCount < this.options.maxRetries) {
573
+ // Try to acquire the lock
574
+ if (this._tryAcquire(lockFile, pidFile)) {
575
+ return true;
576
+ }
577
+
578
+ // Check timeout
579
+ if (Date.now() - startTime >= this.options.timeout) {
580
+ return false;
581
+ }
582
+
583
+ // Wait before retry (ASYNC - yields to event loop)
584
+ retryCount++;
585
+ if (retryCount < this.options.maxRetries) {
586
+ await new Promise(resolve =>
587
+ setTimeout(resolve, this.options.retryInterval)
588
+ );
589
+ }
590
+ }
591
+
592
+ return false;
593
+ }
594
+
595
+ /**
596
+ * Wait for a lock to be released asynchronously (yields to event loop)
597
+ *
598
+ * This is the non-blocking version of waitForLock(). It uses setTimeout
599
+ * for delays, allowing other async operations to run during wait periods.
600
+ *
601
+ * @param {string} filePath - Path to wait for
602
+ * @param {number} maxWait - Maximum milliseconds to wait (default: timeout option)
603
+ * @returns {Promise<boolean>} True if lock was released, false on timeout
604
+ */
605
+ async waitForLockAsync(filePath, maxWait = null) {
606
+ const waitTime = maxWait || this.options.timeout;
607
+ const startTime = Date.now();
608
+
609
+ while (this.isLocked(filePath)) {
610
+ if (Date.now() - startTime >= waitTime) {
611
+ return false;
612
+ }
613
+
614
+ // Small async delay (yields to event loop)
615
+ await new Promise(resolve => setTimeout(resolve, 10));
616
+ }
617
+
618
+ return true;
619
+ }
620
+
386
621
  /**
387
622
  * Force cleanup of all locks in the lock directory
388
623
  *