@eddacraft/anvil-runtime 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 (170) hide show
  1. package/LICENSE +14 -0
  2. package/dist/cache/cache-key.d.ts +45 -0
  3. package/dist/cache/cache-key.d.ts.map +1 -0
  4. package/dist/cache/cache-key.js +135 -0
  5. package/dist/cache/index.d.ts +27 -0
  6. package/dist/cache/index.d.ts.map +1 -0
  7. package/dist/cache/index.js +38 -0
  8. package/dist/cache/providers/file-cache.d.ts +63 -0
  9. package/dist/cache/providers/file-cache.d.ts.map +1 -0
  10. package/dist/cache/providers/file-cache.js +369 -0
  11. package/dist/cache/providers/memory-cache.d.ts +52 -0
  12. package/dist/cache/providers/memory-cache.d.ts.map +1 -0
  13. package/dist/cache/providers/memory-cache.js +197 -0
  14. package/dist/cache/providers/null-cache.d.ts +26 -0
  15. package/dist/cache/providers/null-cache.d.ts.map +1 -0
  16. package/dist/cache/providers/null-cache.js +50 -0
  17. package/dist/cache/types.d.ts +114 -0
  18. package/dist/cache/types.d.ts.map +1 -0
  19. package/dist/cache/types.js +4 -0
  20. package/dist/concurrency/agent.d.ts +137 -0
  21. package/dist/concurrency/agent.d.ts.map +1 -0
  22. package/dist/concurrency/agent.js +440 -0
  23. package/dist/concurrency/atomic.d.ts +93 -0
  24. package/dist/concurrency/atomic.d.ts.map +1 -0
  25. package/dist/concurrency/atomic.js +281 -0
  26. package/dist/concurrency/git-agent.d.ts +114 -0
  27. package/dist/concurrency/git-agent.d.ts.map +1 -0
  28. package/dist/concurrency/git-agent.js +313 -0
  29. package/dist/concurrency/index.d.ts +95 -0
  30. package/dist/concurrency/index.d.ts.map +1 -0
  31. package/dist/concurrency/index.js +127 -0
  32. package/dist/concurrency/lock-manager.d.ts +170 -0
  33. package/dist/concurrency/lock-manager.d.ts.map +1 -0
  34. package/dist/concurrency/lock-manager.js +525 -0
  35. package/dist/concurrency/queue-manager.d.ts +166 -0
  36. package/dist/concurrency/queue-manager.d.ts.map +1 -0
  37. package/dist/concurrency/queue-manager.js +442 -0
  38. package/dist/concurrency/types.d.ts +382 -0
  39. package/dist/concurrency/types.d.ts.map +1 -0
  40. package/dist/concurrency/types.js +204 -0
  41. package/dist/export/constraint-collector.d.ts +175 -0
  42. package/dist/export/constraint-collector.d.ts.map +1 -0
  43. package/dist/export/constraint-collector.js +203 -0
  44. package/dist/export/formatters/llms-txt-formatter.d.ts +89 -0
  45. package/dist/export/formatters/llms-txt-formatter.d.ts.map +1 -0
  46. package/dist/export/formatters/llms-txt-formatter.js +249 -0
  47. package/dist/export/formatters/mcp-resource-formatter.d.ts +186 -0
  48. package/dist/export/formatters/mcp-resource-formatter.d.ts.map +1 -0
  49. package/dist/export/formatters/mcp-resource-formatter.js +139 -0
  50. package/dist/export/formatters/prompt-formatter.d.ts +83 -0
  51. package/dist/export/formatters/prompt-formatter.d.ts.map +1 -0
  52. package/dist/export/formatters/prompt-formatter.js +256 -0
  53. package/dist/export/index.d.ts +10 -0
  54. package/dist/export/index.d.ts.map +1 -0
  55. package/dist/export/index.js +9 -0
  56. package/dist/gate/check.interface.d.ts +15 -0
  57. package/dist/gate/check.interface.d.ts.map +1 -0
  58. package/dist/gate/check.interface.js +18 -0
  59. package/dist/gate/checks/antipattern.check.d.ts +27 -0
  60. package/dist/gate/checks/antipattern.check.d.ts.map +1 -0
  61. package/dist/gate/checks/antipattern.check.js +140 -0
  62. package/dist/gate/checks/architecture/circular-detector.d.ts +33 -0
  63. package/dist/gate/checks/architecture/circular-detector.d.ts.map +1 -0
  64. package/dist/gate/checks/architecture/circular-detector.js +71 -0
  65. package/dist/gate/checks/architecture/dependency-analyzer.d.ts +81 -0
  66. package/dist/gate/checks/architecture/dependency-analyzer.d.ts.map +1 -0
  67. package/dist/gate/checks/architecture/dependency-analyzer.js +136 -0
  68. package/dist/gate/checks/architecture/layer-validator.d.ts +75 -0
  69. package/dist/gate/checks/architecture/layer-validator.d.ts.map +1 -0
  70. package/dist/gate/checks/architecture/layer-validator.js +193 -0
  71. package/dist/gate/checks/architecture.check.d.ts +56 -0
  72. package/dist/gate/checks/architecture.check.d.ts.map +1 -0
  73. package/dist/gate/checks/architecture.check.js +394 -0
  74. package/dist/gate/checks/command-safety.check.d.ts +12 -0
  75. package/dist/gate/checks/command-safety.check.d.ts.map +1 -0
  76. package/dist/gate/checks/command-safety.check.js +230 -0
  77. package/dist/gate/checks/coverage.check.d.ts +9 -0
  78. package/dist/gate/checks/coverage.check.d.ts.map +1 -0
  79. package/dist/gate/checks/coverage.check.js +81 -0
  80. package/dist/gate/checks/dependency.check.d.ts +17 -0
  81. package/dist/gate/checks/dependency.check.d.ts.map +1 -0
  82. package/dist/gate/checks/dependency.check.js +342 -0
  83. package/dist/gate/checks/eslint.check.d.ts +14 -0
  84. package/dist/gate/checks/eslint.check.d.ts.map +1 -0
  85. package/dist/gate/checks/eslint.check.js +79 -0
  86. package/dist/gate/checks/policy.check.d.ts +78 -0
  87. package/dist/gate/checks/policy.check.d.ts.map +1 -0
  88. package/dist/gate/checks/policy.check.js +457 -0
  89. package/dist/gate/checks/secret/entropy-detector.d.ts +44 -0
  90. package/dist/gate/checks/secret/entropy-detector.d.ts.map +1 -0
  91. package/dist/gate/checks/secret/entropy-detector.js +76 -0
  92. package/dist/gate/checks/secret/git-scanner.d.ts +36 -0
  93. package/dist/gate/checks/secret/git-scanner.d.ts.map +1 -0
  94. package/dist/gate/checks/secret/git-scanner.js +90 -0
  95. package/dist/gate/checks/secret/secret-patterns.d.ts +42 -0
  96. package/dist/gate/checks/secret/secret-patterns.d.ts.map +1 -0
  97. package/dist/gate/checks/secret/secret-patterns.js +137 -0
  98. package/dist/gate/checks/secret.check.d.ts +56 -0
  99. package/dist/gate/checks/secret.check.d.ts.map +1 -0
  100. package/dist/gate/checks/secret.check.js +245 -0
  101. package/dist/gate/config/command-safety-config.d.ts +5 -0
  102. package/dist/gate/config/command-safety-config.d.ts.map +1 -0
  103. package/dist/gate/config/command-safety-config.js +69 -0
  104. package/dist/gate/config/index.d.ts +2 -0
  105. package/dist/gate/config/index.d.ts.map +1 -0
  106. package/dist/gate/config/index.js +1 -0
  107. package/dist/gate/formatters/command-safety-formatter.d.ts +10 -0
  108. package/dist/gate/formatters/command-safety-formatter.d.ts.map +1 -0
  109. package/dist/gate/formatters/command-safety-formatter.js +64 -0
  110. package/dist/gate/formatters/index.d.ts +2 -0
  111. package/dist/gate/formatters/index.d.ts.map +1 -0
  112. package/dist/gate/formatters/index.js +1 -0
  113. package/dist/gate/gate-config.d.ts +44 -0
  114. package/dist/gate/gate-config.d.ts.map +1 -0
  115. package/dist/gate/gate-config.js +334 -0
  116. package/dist/gate/gate-runner.d.ts +160 -0
  117. package/dist/gate/gate-runner.d.ts.map +1 -0
  118. package/dist/gate/gate-runner.js +531 -0
  119. package/dist/gate/index.d.ts +20 -0
  120. package/dist/gate/index.d.ts.map +1 -0
  121. package/dist/gate/index.js +14 -0
  122. package/dist/gate/parsers/command-parser.d.ts +18 -0
  123. package/dist/gate/parsers/command-parser.d.ts.map +1 -0
  124. package/dist/gate/parsers/command-parser.js +363 -0
  125. package/dist/gate/parsers/index.d.ts +2 -0
  126. package/dist/gate/parsers/index.d.ts.map +1 -0
  127. package/dist/gate/parsers/index.js +1 -0
  128. package/dist/gate/policy/index.d.ts +12 -0
  129. package/dist/gate/policy/index.d.ts.map +1 -0
  130. package/dist/gate/policy/index.js +10 -0
  131. package/dist/gate/rules/default-filesystem-rules.d.ts +3 -0
  132. package/dist/gate/rules/default-filesystem-rules.d.ts.map +1 -0
  133. package/dist/gate/rules/default-filesystem-rules.js +201 -0
  134. package/dist/gate/rules/default-git-rules.d.ts +3 -0
  135. package/dist/gate/rules/default-git-rules.d.ts.map +1 -0
  136. package/dist/gate/rules/default-git-rules.js +192 -0
  137. package/dist/gate/rules/index.d.ts +5 -0
  138. package/dist/gate/rules/index.d.ts.map +1 -0
  139. package/dist/gate/rules/index.js +3 -0
  140. package/dist/gate/rules/rule-matcher.d.ts +27 -0
  141. package/dist/gate/rules/rule-matcher.d.ts.map +1 -0
  142. package/dist/gate/rules/rule-matcher.js +228 -0
  143. package/dist/gate/rules/types.d.ts +250 -0
  144. package/dist/gate/rules/types.d.ts.map +1 -0
  145. package/dist/gate/rules/types.js +1 -0
  146. package/dist/index.d.ts +19 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +35 -0
  149. package/dist/types/gate.types.d.ts +42 -0
  150. package/dist/types/gate.types.d.ts.map +1 -0
  151. package/dist/types/gate.types.js +94 -0
  152. package/dist/watch/debouncer.d.ts +90 -0
  153. package/dist/watch/debouncer.d.ts.map +1 -0
  154. package/dist/watch/debouncer.js +135 -0
  155. package/dist/watch/file-watcher.d.ts +73 -0
  156. package/dist/watch/file-watcher.d.ts.map +1 -0
  157. package/dist/watch/file-watcher.js +121 -0
  158. package/dist/watch/git-status.d.ts +98 -0
  159. package/dist/watch/git-status.d.ts.map +1 -0
  160. package/dist/watch/git-status.js +266 -0
  161. package/dist/watch/index.d.ts +16 -0
  162. package/dist/watch/index.d.ts.map +1 -0
  163. package/dist/watch/index.js +15 -0
  164. package/dist/watch/orchestrator.d.ts +113 -0
  165. package/dist/watch/orchestrator.d.ts.map +1 -0
  166. package/dist/watch/orchestrator.js +409 -0
  167. package/dist/watch/types.d.ts +190 -0
  168. package/dist/watch/types.d.ts.map +1 -0
  169. package/dist/watch/types.js +76 -0
  170. package/package.json +60 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Lock Manager
3
+ *
4
+ * Provides distributed locking for multi-agent coordination using file-based locks.
5
+ * Supports automatic stale lock detection and recovery.
6
+ */
7
+ import { type LockRecord, type LockType, type LockAcquisitionResult, type LockReleaseResult, type AgentInfo, type ConcurrencyConfig } from './types.js';
8
+ /**
9
+ * Options for LockManager
10
+ */
11
+ export interface LockManagerOptions {
12
+ /** Workspace root directory */
13
+ workspaceRoot: string;
14
+ /** Concurrency configuration */
15
+ config?: Partial<ConcurrencyConfig>;
16
+ /** Agent info for this lock manager */
17
+ agentInfo?: AgentInfo;
18
+ }
19
+ /**
20
+ * Options for lock acquisition
21
+ */
22
+ export interface AcquireLockOptions {
23
+ /** Lock type */
24
+ type: LockType;
25
+ /** Resource being locked */
26
+ resource: string;
27
+ /** Reason for lock */
28
+ reason?: string;
29
+ /** Custom timeout (overrides config) */
30
+ timeoutMs?: number;
31
+ /** Wait for lock (block until acquired or timeout) */
32
+ wait?: boolean;
33
+ /** Wait timeout in ms (default: 30s) */
34
+ waitTimeoutMs?: number;
35
+ /** Retry interval in ms (default: 100ms) */
36
+ retryIntervalMs?: number;
37
+ /** Allow acquiring from stale agents */
38
+ acquireFromStale?: boolean;
39
+ }
40
+ /**
41
+ * Lock Manager
42
+ *
43
+ * Manages distributed file-based locks for multi-agent coordination.
44
+ */
45
+ export declare class LockManager {
46
+ private readonly workspaceRoot;
47
+ private readonly config;
48
+ private readonly agent;
49
+ private readonly lockDir;
50
+ private readonly heldLocks;
51
+ private readonly renewalTimers;
52
+ constructor(options: LockManagerOptions);
53
+ /**
54
+ * Get the agent ID
55
+ */
56
+ getAgentId(): string;
57
+ /**
58
+ * Acquire a lock
59
+ */
60
+ acquire(options: AcquireLockOptions): Promise<LockAcquisitionResult>;
61
+ /**
62
+ * Acquire lock with wait/retry
63
+ */
64
+ private acquireWithWait;
65
+ /**
66
+ * Release a lock
67
+ */
68
+ release(type: LockType, resource: string): Promise<LockReleaseResult>;
69
+ /**
70
+ * Release all locks held by this manager
71
+ */
72
+ releaseAll(): Promise<void>;
73
+ /**
74
+ * Check if a lock is held
75
+ */
76
+ isLocked(type: LockType, resource: string): Promise<boolean>;
77
+ /**
78
+ * Get lock info
79
+ */
80
+ getLockInfo(type: LockType, resource: string): Promise<LockRecord | null>;
81
+ /**
82
+ * Check if we hold a lock
83
+ */
84
+ holdsLock(type: LockType, resource: string): boolean;
85
+ /**
86
+ * Get all locks held by this manager
87
+ */
88
+ getHeldLocks(): LockRecord[];
89
+ /**
90
+ * Start auto-renewal for a lock
91
+ */
92
+ startAutoRenewal(type: LockType, resource: string, intervalMs?: number): void;
93
+ /**
94
+ * Stop auto-renewal for a lock
95
+ */
96
+ stopRenewal(lockKey: string): void;
97
+ /**
98
+ * Stop all auto-renewals
99
+ */
100
+ stopAllRenewals(): void;
101
+ /**
102
+ * Cleanup expired locks in the lock directory
103
+ */
104
+ cleanupExpiredLocks(): Promise<number>;
105
+ /**
106
+ * Create a new lock
107
+ */
108
+ private createLock;
109
+ /**
110
+ * Renew an existing lock
111
+ */
112
+ private renewLock;
113
+ /**
114
+ * Force acquire a lock (taking over from expired/stale holder)
115
+ */
116
+ private forceAcquire;
117
+ /**
118
+ * Check if lock holder is stale
119
+ */
120
+ private isHolderStale;
121
+ /**
122
+ * Read lock file
123
+ */
124
+ private readLock;
125
+ /**
126
+ * Get lock key from type and resource
127
+ */
128
+ private getLockKey;
129
+ /**
130
+ * Get lock file path
131
+ */
132
+ private getLockPath;
133
+ /**
134
+ * Ensure lock directory exists
135
+ */
136
+ private ensureLockDir;
137
+ }
138
+ /**
139
+ * Create a lock manager
140
+ */
141
+ export declare function createLockManager(options: LockManagerOptions): LockManager;
142
+ /**
143
+ * Execute a function while holding a lock
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * const result = await withLock(
148
+ * lockManager,
149
+ * { type: 'action', resource: 'gate' },
150
+ * async () => {
151
+ * // Perform work that requires exclusive access
152
+ * return await runGates();
153
+ * }
154
+ * );
155
+ * ```
156
+ */
157
+ export declare function withLock<T>(manager: LockManager, options: Omit<AcquireLockOptions, 'wait'>, fn: () => Promise<T>): Promise<T>;
158
+ /**
159
+ * Try to execute a function while holding a lock (non-blocking)
160
+ *
161
+ * Returns null if lock couldn't be acquired.
162
+ */
163
+ export declare function tryWithLock<T>(manager: LockManager, options: Omit<AcquireLockOptions, 'wait'>, fn: () => Promise<T>): Promise<{
164
+ success: true;
165
+ result: T;
166
+ } | {
167
+ success: false;
168
+ heldBy?: LockAcquisitionResult['heldBy'];
169
+ }>;
170
+ //# sourceMappingURL=lock-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-manager.d.ts","sourceRoot":"","sources":["../../src/concurrency/lock-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,525 @@
1
+ /**
2
+ * Lock Manager
3
+ *
4
+ * Provides distributed locking for multi-agent coordination using file-based locks.
5
+ * Supports automatic stale lock detection and recovery.
6
+ */
7
+ import { promises as fs } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { createHash } from 'node:crypto';
10
+ import { LockFileSchema, getDefaultConcurrencyConfig, } from './types.js';
11
+ import { atomicWriteJson, readJsonSafe, unlinkSafe, sleepWithJitter, tryAcquireFileLock, } from './atomic.js';
12
+ import { createAgentInfo } from './agent.js';
13
+ import { createDebugger } from '@eddacraft/anvil-core';
14
+ const debug = createDebugger('lock');
15
+ /**
16
+ * Lock Manager
17
+ *
18
+ * Manages distributed file-based locks for multi-agent coordination.
19
+ */
20
+ export class LockManager {
21
+ workspaceRoot;
22
+ config;
23
+ agent;
24
+ lockDir;
25
+ // Track locks held by this manager instance
26
+ heldLocks = new Map();
27
+ // Renewal timers
28
+ renewalTimers = new Map();
29
+ constructor(options) {
30
+ this.workspaceRoot = options.workspaceRoot;
31
+ this.config = {
32
+ ...getDefaultConcurrencyConfig(),
33
+ ...options.config,
34
+ };
35
+ this.agent = options.agentInfo ?? createAgentInfo();
36
+ this.lockDir = join(this.workspaceRoot, this.config.lockDir);
37
+ }
38
+ /**
39
+ * Get the agent ID
40
+ */
41
+ getAgentId() {
42
+ return this.agent.id;
43
+ }
44
+ /**
45
+ * Acquire a lock
46
+ */
47
+ async acquire(options) {
48
+ const { type, resource, reason, timeoutMs = this.config.lockTimeoutMs, wait = false, waitTimeoutMs = 30000, retryIntervalMs = 100, acquireFromStale = this.config.autoAcquireFromStale, } = options;
49
+ const lockKey = this.getLockKey(type, resource);
50
+ const lockPath = this.getLockPath(lockKey);
51
+ // If waiting, use retry loop
52
+ if (wait) {
53
+ return this.acquireWithWait(options, waitTimeoutMs, retryIntervalMs);
54
+ }
55
+ await this.ensureLockDir();
56
+ // Check for existing lock
57
+ const existingLock = await this.readLock(lockPath);
58
+ if (existingLock) {
59
+ // Check if it's our lock
60
+ if (existingLock.lock.agentId === this.agent.id) {
61
+ // Renew our existing lock
62
+ return this.renewLock(lockPath, existingLock, reason);
63
+ }
64
+ // Check if lock is expired
65
+ const now = Date.now();
66
+ const expiresAt = new Date(existingLock.lock.expiresAt).getTime();
67
+ if (now > expiresAt) {
68
+ debug(`Lock expired, taking over: ${lockKey}`);
69
+ return this.forceAcquire(lockPath, type, resource, reason, timeoutMs, existingLock);
70
+ }
71
+ // Check if holder is stale
72
+ if (acquireFromStale && (await this.isHolderStale(existingLock.lock))) {
73
+ debug(`Lock holder is stale, taking over: ${lockKey}`);
74
+ return this.forceAcquire(lockPath, type, resource, reason, timeoutMs, existingLock);
75
+ }
76
+ // Lock is held by another active agent
77
+ return {
78
+ acquired: false,
79
+ error: `Lock held by ${existingLock.lock.agentId}`,
80
+ heldBy: {
81
+ agentId: existingLock.lock.agentId,
82
+ agentType: existingLock.lock.agentType,
83
+ acquiredAt: existingLock.lock.acquiredAt,
84
+ expiresAt: existingLock.lock.expiresAt,
85
+ pid: existingLock.lock.pid,
86
+ },
87
+ };
88
+ }
89
+ // No existing lock, acquire it
90
+ return this.createLock(lockPath, type, resource, reason, timeoutMs);
91
+ }
92
+ /**
93
+ * Acquire lock with wait/retry
94
+ */
95
+ async acquireWithWait(options, waitTimeoutMs, retryIntervalMs) {
96
+ const startTime = Date.now();
97
+ while (Date.now() - startTime < waitTimeoutMs) {
98
+ const result = await this.acquire({ ...options, wait: false });
99
+ if (result.acquired) {
100
+ return result;
101
+ }
102
+ // Wait with jitter before retrying
103
+ await sleepWithJitter(retryIntervalMs);
104
+ }
105
+ // Timeout
106
+ const lockKey = this.getLockKey(options.type, options.resource);
107
+ const existingLock = await this.readLock(this.getLockPath(lockKey));
108
+ return {
109
+ acquired: false,
110
+ error: `Lock acquisition timed out after ${waitTimeoutMs}ms`,
111
+ heldBy: existingLock
112
+ ? {
113
+ agentId: existingLock.lock.agentId,
114
+ agentType: existingLock.lock.agentType,
115
+ acquiredAt: existingLock.lock.acquiredAt,
116
+ expiresAt: existingLock.lock.expiresAt,
117
+ pid: existingLock.lock.pid,
118
+ }
119
+ : undefined,
120
+ };
121
+ }
122
+ /**
123
+ * Release a lock
124
+ */
125
+ async release(type, resource) {
126
+ const lockKey = this.getLockKey(type, resource);
127
+ const lockPath = this.getLockPath(lockKey);
128
+ // Stop renewal timer
129
+ this.stopRenewal(lockKey);
130
+ const existingLock = await this.readLock(lockPath);
131
+ if (!existingLock) {
132
+ this.heldLocks.delete(lockKey);
133
+ return { released: true };
134
+ }
135
+ // Check if we own the lock
136
+ if (existingLock.lock.agentId !== this.agent.id) {
137
+ return {
138
+ released: false,
139
+ error: `Lock held by different agent: ${existingLock.lock.agentId}`,
140
+ wasHeldByOther: true,
141
+ };
142
+ }
143
+ // Delete the lock file
144
+ await unlinkSafe(lockPath);
145
+ this.heldLocks.delete(lockKey);
146
+ debug(`Lock released: ${lockKey}`);
147
+ return { released: true };
148
+ }
149
+ /**
150
+ * Release all locks held by this manager
151
+ */
152
+ async releaseAll() {
153
+ for (const [, lock] of this.heldLocks) {
154
+ await this.release(lock.type, lock.resource);
155
+ }
156
+ }
157
+ /**
158
+ * Check if a lock is held
159
+ */
160
+ async isLocked(type, resource) {
161
+ const lockKey = this.getLockKey(type, resource);
162
+ const lockPath = this.getLockPath(lockKey);
163
+ const existingLock = await this.readLock(lockPath);
164
+ if (!existingLock) {
165
+ return false;
166
+ }
167
+ // Check expiration
168
+ const now = Date.now();
169
+ const expiresAt = new Date(existingLock.lock.expiresAt).getTime();
170
+ return now <= expiresAt;
171
+ }
172
+ /**
173
+ * Get lock info
174
+ */
175
+ async getLockInfo(type, resource) {
176
+ const lockKey = this.getLockKey(type, resource);
177
+ const lockPath = this.getLockPath(lockKey);
178
+ const existingLock = await this.readLock(lockPath);
179
+ return existingLock?.lock ?? null;
180
+ }
181
+ /**
182
+ * Check if we hold a lock
183
+ */
184
+ holdsLock(type, resource) {
185
+ const lockKey = this.getLockKey(type, resource);
186
+ return this.heldLocks.has(lockKey);
187
+ }
188
+ /**
189
+ * Get all locks held by this manager
190
+ */
191
+ getHeldLocks() {
192
+ return Array.from(this.heldLocks.values());
193
+ }
194
+ /**
195
+ * Start auto-renewal for a lock
196
+ */
197
+ startAutoRenewal(type, resource, intervalMs) {
198
+ const lockKey = this.getLockKey(type, resource);
199
+ const interval = intervalMs ?? Math.floor(this.config.lockTimeoutMs / 3);
200
+ if (this.renewalTimers.has(lockKey)) {
201
+ return;
202
+ }
203
+ const timer = setInterval(async () => {
204
+ try {
205
+ const lockPath = this.getLockPath(lockKey);
206
+ const existingLock = await this.readLock(lockPath);
207
+ if (existingLock && existingLock.lock.agentId === this.agent.id) {
208
+ await this.renewLock(lockPath, existingLock);
209
+ debug(`Lock auto-renewed: ${lockKey}`);
210
+ }
211
+ else {
212
+ // We lost the lock
213
+ this.stopRenewal(lockKey);
214
+ this.heldLocks.delete(lockKey);
215
+ debug(`Lost lock, stopping renewal: ${lockKey}`);
216
+ }
217
+ }
218
+ catch (error) {
219
+ debug(`Auto-renewal failed for ${lockKey}:`, error);
220
+ }
221
+ }, interval);
222
+ // Don't block process exit
223
+ timer.unref();
224
+ this.renewalTimers.set(lockKey, timer);
225
+ debug(`Auto-renewal started for ${lockKey} (interval=${interval}ms)`);
226
+ }
227
+ /**
228
+ * Stop auto-renewal for a lock
229
+ */
230
+ stopRenewal(lockKey) {
231
+ const timer = this.renewalTimers.get(lockKey);
232
+ if (timer) {
233
+ clearInterval(timer);
234
+ this.renewalTimers.delete(lockKey);
235
+ debug(`Auto-renewal stopped for ${lockKey}`);
236
+ }
237
+ }
238
+ /**
239
+ * Stop all auto-renewals
240
+ */
241
+ stopAllRenewals() {
242
+ for (const [lockKey, timer] of this.renewalTimers) {
243
+ clearInterval(timer);
244
+ debug(`Auto-renewal stopped for ${lockKey}`);
245
+ }
246
+ this.renewalTimers.clear();
247
+ }
248
+ /**
249
+ * Cleanup expired locks in the lock directory
250
+ */
251
+ async cleanupExpiredLocks() {
252
+ let cleaned = 0;
253
+ try {
254
+ const files = await fs.readdir(this.lockDir);
255
+ for (const file of files) {
256
+ if (!file.endsWith('.lock'))
257
+ continue;
258
+ const lockPath = join(this.lockDir, file);
259
+ const lock = await this.readLock(lockPath);
260
+ if (!lock)
261
+ continue;
262
+ const now = Date.now();
263
+ const expiresAt = new Date(lock.lock.expiresAt).getTime();
264
+ if (now > expiresAt) {
265
+ await unlinkSafe(lockPath);
266
+ cleaned++;
267
+ debug(`Cleaned up expired lock: ${file}`);
268
+ }
269
+ }
270
+ }
271
+ catch (error) {
272
+ if (error.code !== 'ENOENT') {
273
+ debug('Cleanup failed:', error);
274
+ }
275
+ }
276
+ return cleaned;
277
+ }
278
+ // ============================================================================
279
+ // Private Methods
280
+ // ============================================================================
281
+ /**
282
+ * Create a new lock
283
+ */
284
+ async createLock(lockPath, type, resource, reason, timeoutMs) {
285
+ // Use O_EXCL sentinel to prevent two agents both creating the lock
286
+ const sentinelPath = `${lockPath}.creating`;
287
+ const sentinel = await tryAcquireFileLock(sentinelPath, this.agent.id);
288
+ if (!sentinel) {
289
+ // Another agent is creating the lock right now — read what they wrote
290
+ const existingLock = await this.readLock(lockPath);
291
+ if (existingLock) {
292
+ return {
293
+ acquired: false,
294
+ error: `Lock acquired by another agent: ${existingLock.lock.agentId}`,
295
+ heldBy: {
296
+ agentId: existingLock.lock.agentId,
297
+ agentType: existingLock.lock.agentType,
298
+ acquiredAt: existingLock.lock.acquiredAt,
299
+ expiresAt: existingLock.lock.expiresAt,
300
+ pid: existingLock.lock.pid,
301
+ },
302
+ };
303
+ }
304
+ return {
305
+ acquired: false,
306
+ error: 'Lock creation in progress by another agent',
307
+ };
308
+ }
309
+ try {
310
+ // Re-check after acquiring sentinel (another agent may have finished first)
311
+ const raceCheck = await this.readLock(lockPath);
312
+ if (raceCheck && raceCheck.lock.agentId !== this.agent.id) {
313
+ return {
314
+ acquired: false,
315
+ error: `Lock acquired by another agent: ${raceCheck.lock.agentId}`,
316
+ heldBy: {
317
+ agentId: raceCheck.lock.agentId,
318
+ agentType: raceCheck.lock.agentType,
319
+ acquiredAt: raceCheck.lock.acquiredAt,
320
+ expiresAt: raceCheck.lock.expiresAt,
321
+ pid: raceCheck.lock.pid,
322
+ },
323
+ };
324
+ }
325
+ const now = new Date();
326
+ const expiresAt = new Date(now.getTime() + timeoutMs);
327
+ const lock = {
328
+ type,
329
+ resource,
330
+ agentId: this.agent.id,
331
+ agentType: this.agent.type,
332
+ pid: this.agent.pid,
333
+ acquiredAt: now.toISOString(),
334
+ expiresAt: expiresAt.toISOString(),
335
+ reason,
336
+ renewCount: 0,
337
+ };
338
+ const lockFile = {
339
+ version: '1.0.0',
340
+ lock,
341
+ history: [],
342
+ };
343
+ await atomicWriteJson(lockPath, lockFile);
344
+ const lockKey = this.getLockKey(type, resource);
345
+ this.heldLocks.set(lockKey, lock);
346
+ debug(`Lock acquired: ${lockKey}`);
347
+ return {
348
+ acquired: true,
349
+ lock,
350
+ };
351
+ }
352
+ finally {
353
+ await sentinel.release();
354
+ }
355
+ }
356
+ /**
357
+ * Renew an existing lock
358
+ */
359
+ async renewLock(lockPath, existingLock, reason) {
360
+ const now = new Date();
361
+ const expiresAt = new Date(now.getTime() + this.config.lockTimeoutMs);
362
+ const lock = {
363
+ ...existingLock.lock,
364
+ expiresAt: expiresAt.toISOString(),
365
+ renewCount: existingLock.lock.renewCount + 1,
366
+ reason: reason ?? existingLock.lock.reason,
367
+ };
368
+ const lockFile = {
369
+ ...existingLock,
370
+ lock,
371
+ };
372
+ await atomicWriteJson(lockPath, lockFile);
373
+ const lockKey = this.getLockKey(lock.type, lock.resource);
374
+ this.heldLocks.set(lockKey, lock);
375
+ debug(`Lock renewed: ${lockKey} (count=${lock.renewCount})`);
376
+ return {
377
+ acquired: true,
378
+ lock,
379
+ };
380
+ }
381
+ /**
382
+ * Force acquire a lock (taking over from expired/stale holder)
383
+ */
384
+ async forceAcquire(lockPath, type, resource, reason, timeoutMs, existingLock) {
385
+ const now = new Date();
386
+ const expiresAt = new Date(now.getTime() + timeoutMs);
387
+ const lock = {
388
+ type,
389
+ resource,
390
+ agentId: this.agent.id,
391
+ agentType: this.agent.type,
392
+ pid: this.agent.pid,
393
+ acquiredAt: now.toISOString(),
394
+ expiresAt: expiresAt.toISOString(),
395
+ reason,
396
+ renewCount: 0,
397
+ };
398
+ const lockFile = {
399
+ version: '1.0.0',
400
+ lock,
401
+ history: [
402
+ ...existingLock.history,
403
+ {
404
+ agentId: existingLock.lock.agentId,
405
+ acquiredAt: existingLock.lock.acquiredAt,
406
+ releasedAt: now.toISOString(),
407
+ reason: 'Force-released (expired/stale)',
408
+ },
409
+ ],
410
+ };
411
+ await atomicWriteJson(lockPath, lockFile);
412
+ const lockKey = this.getLockKey(type, resource);
413
+ this.heldLocks.set(lockKey, lock);
414
+ debug(`Lock force-acquired: ${lockKey}`);
415
+ return {
416
+ acquired: true,
417
+ lock,
418
+ };
419
+ }
420
+ /**
421
+ * Check if lock holder is stale
422
+ */
423
+ async isHolderStale(lock) {
424
+ // Check if process is still running
425
+ if (lock.pid) {
426
+ try {
427
+ process.kill(lock.pid, 0);
428
+ return false; // Process exists
429
+ }
430
+ catch {
431
+ return true; // Process doesn't exist
432
+ }
433
+ }
434
+ // Can't determine - assume not stale
435
+ return false;
436
+ }
437
+ /**
438
+ * Read lock file
439
+ */
440
+ async readLock(lockPath) {
441
+ const data = await readJsonSafe(lockPath);
442
+ if (!data)
443
+ return null;
444
+ const result = LockFileSchema.safeParse(data);
445
+ if (!result.success) {
446
+ debug('Invalid lock file schema:', result.error);
447
+ return null;
448
+ }
449
+ return result.data;
450
+ }
451
+ /**
452
+ * Get lock key from type and resource
453
+ */
454
+ getLockKey(type, resource) {
455
+ return `${type}:${resource}`;
456
+ }
457
+ /**
458
+ * Get lock file path
459
+ */
460
+ getLockPath(lockKey) {
461
+ // Hash the key for safe filename
462
+ const hash = createHash('sha256').update(lockKey).digest('hex').slice(0, 16);
463
+ return join(this.lockDir, `${hash}.lock`);
464
+ }
465
+ /**
466
+ * Ensure lock directory exists
467
+ */
468
+ async ensureLockDir() {
469
+ await fs.mkdir(this.lockDir, { recursive: true });
470
+ }
471
+ }
472
+ /**
473
+ * Create a lock manager
474
+ */
475
+ export function createLockManager(options) {
476
+ return new LockManager(options);
477
+ }
478
+ // ============================================================================
479
+ // Convenience Functions
480
+ // ============================================================================
481
+ /**
482
+ * Execute a function while holding a lock
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * const result = await withLock(
487
+ * lockManager,
488
+ * { type: 'action', resource: 'gate' },
489
+ * async () => {
490
+ * // Perform work that requires exclusive access
491
+ * return await runGates();
492
+ * }
493
+ * );
494
+ * ```
495
+ */
496
+ export async function withLock(manager, options, fn) {
497
+ const result = await manager.acquire({ ...options, wait: true });
498
+ if (!result.acquired) {
499
+ throw new Error(`Failed to acquire lock: ${result.error}`);
500
+ }
501
+ try {
502
+ return await fn();
503
+ }
504
+ finally {
505
+ await manager.release(options.type, options.resource);
506
+ }
507
+ }
508
+ /**
509
+ * Try to execute a function while holding a lock (non-blocking)
510
+ *
511
+ * Returns null if lock couldn't be acquired.
512
+ */
513
+ export async function tryWithLock(manager, options, fn) {
514
+ const result = await manager.acquire({ ...options, wait: false });
515
+ if (!result.acquired) {
516
+ return { success: false, heldBy: result.heldBy };
517
+ }
518
+ try {
519
+ const fnResult = await fn();
520
+ return { success: true, result: fnResult };
521
+ }
522
+ finally {
523
+ await manager.release(options.type, options.resource);
524
+ }
525
+ }