@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,166 @@
1
+ /**
2
+ * Queue Manager
3
+ *
4
+ * Provides fair queuing for resources when multiple agents are waiting
5
+ * for the same lock. Supports priority-based scheduling and timeout handling.
6
+ */
7
+ import { type QueueJoinResult, type QueueStatusResult, type LockType, type AgentInfo, type ConcurrencyConfig, type LockAcquisitionResult } from './types.js';
8
+ import { LockManager } from './lock-manager.js';
9
+ /**
10
+ * Options for QueueManager
11
+ */
12
+ export interface QueueManagerOptions {
13
+ /** Workspace root directory */
14
+ workspaceRoot: string;
15
+ /** Concurrency configuration */
16
+ config?: Partial<ConcurrencyConfig>;
17
+ /** Agent info */
18
+ agentInfo?: AgentInfo;
19
+ /** Lock manager (optional, created if not provided) */
20
+ lockManager?: LockManager;
21
+ }
22
+ /**
23
+ * Options for joining a queue
24
+ */
25
+ export interface QueueJoinOptions {
26
+ /** Lock type */
27
+ type: LockType;
28
+ /** Resource identifier */
29
+ resource: string;
30
+ /** Priority (lower = higher priority, default: 100) */
31
+ priority?: number;
32
+ /** Reason for waiting */
33
+ reason?: string;
34
+ /** Custom timeout (overrides config) */
35
+ timeoutMs?: number;
36
+ }
37
+ /**
38
+ * Options for waiting in queue
39
+ */
40
+ export interface QueueWaitOptions extends QueueJoinOptions {
41
+ /** Maximum time to wait in queue (ms) */
42
+ maxWaitMs?: number;
43
+ /** Polling interval (ms) */
44
+ pollIntervalMs?: number;
45
+ /** Callback when position changes */
46
+ onPositionChange?: (position: number, total: number) => void;
47
+ /** Callback when lock is acquired */
48
+ onLockAcquired?: () => void;
49
+ }
50
+ /**
51
+ * Queue Manager
52
+ *
53
+ * Manages fair queuing for resources across multiple agents.
54
+ */
55
+ export declare class QueueManager {
56
+ private readonly workspaceRoot;
57
+ private readonly config;
58
+ private readonly agent;
59
+ private readonly queueDir;
60
+ private readonly lockManager;
61
+ constructor(options: QueueManagerOptions);
62
+ /**
63
+ * Get the agent ID
64
+ */
65
+ getAgentId(): string;
66
+ /**
67
+ * Join a queue for a resource
68
+ */
69
+ join(options: QueueJoinOptions): Promise<QueueJoinResult>;
70
+ /**
71
+ * Leave a queue
72
+ */
73
+ leave(type: LockType, resource: string): Promise<boolean>;
74
+ /**
75
+ * Get queue status
76
+ */
77
+ getStatus(type: LockType, resource: string): Promise<QueueStatusResult>;
78
+ /**
79
+ * Check if it's our turn (we're first in queue)
80
+ */
81
+ isOurTurn(type: LockType, resource: string): Promise<boolean>;
82
+ /**
83
+ * Wait in queue until lock is acquired
84
+ *
85
+ * This is the main coordination primitive - joins queue, waits for turn,
86
+ * then acquires lock.
87
+ */
88
+ waitForLock(options: QueueWaitOptions): Promise<LockAcquisitionResult>;
89
+ /**
90
+ * Get all queues in the workspace
91
+ */
92
+ getAllQueues(): Promise<Array<{
93
+ type: LockType;
94
+ resource: string;
95
+ entries: number;
96
+ }>>;
97
+ /**
98
+ * Cleanup all timed-out entries across all queues
99
+ */
100
+ cleanupAllTimedOut(): Promise<number>;
101
+ /**
102
+ * Load queue from file
103
+ */
104
+ private loadQueue;
105
+ /**
106
+ * Sort queue entries by priority then by queued time
107
+ */
108
+ private sortQueue;
109
+ /**
110
+ * Cleanup timed-out entries
111
+ */
112
+ private cleanupTimedOut;
113
+ /**
114
+ * Get queue file path
115
+ */
116
+ private getQueuePath;
117
+ /**
118
+ * Ensure queue directory exists
119
+ */
120
+ private ensureQueueDir;
121
+ }
122
+ /**
123
+ * Create a queue manager
124
+ */
125
+ export declare function createQueueManager(options: QueueManagerOptions): QueueManager;
126
+ /**
127
+ * Coordinated execution with fair queuing
128
+ *
129
+ * This is the primary way to execute code that requires exclusive access
130
+ * to a resource in a multi-agent environment.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const result = await coordinatedExecution(
135
+ * queueManager,
136
+ * { type: 'action', resource: 'gate', reason: 'Running quality gates' },
137
+ * async () => {
138
+ * return await runGates();
139
+ * }
140
+ * );
141
+ * ```
142
+ */
143
+ export declare function coordinatedExecution<T>(manager: QueueManager, options: QueueWaitOptions, fn: () => Promise<T>): Promise<T>;
144
+ /**
145
+ * Result of a concurrent group execution
146
+ */
147
+ export interface ConcurrentGroupResult<T> {
148
+ /** Results from each execution */
149
+ results: Array<{
150
+ agentId: string;
151
+ result?: T;
152
+ error?: Error;
153
+ }>;
154
+ /** Total execution time */
155
+ totalTimeMs: number;
156
+ /** Number of successful executions */
157
+ successCount: number;
158
+ }
159
+ /**
160
+ * Execute function with coordination, allowing limited concurrency
161
+ *
162
+ * Useful when you want to allow some parallel execution but limit the
163
+ * total number of concurrent agents.
164
+ */
165
+ export declare function withConcurrencyLimit<T>(manager: QueueManager, type: LockType, resource: string, maxConcurrent: number, fn: () => Promise<T>): Promise<T>;
166
+ //# sourceMappingURL=queue-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-manager.d.ts","sourceRoot":"","sources":["../../src/concurrency/queue-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Queue Manager
3
+ *
4
+ * Provides fair queuing for resources when multiple agents are waiting
5
+ * for the same lock. Supports priority-based scheduling and timeout handling.
6
+ */
7
+ import { promises as fs } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { createHash, randomUUID } from 'node:crypto';
10
+ import { QueueFileSchema, getDefaultConcurrencyConfig, } from './types.js';
11
+ import { atomicWriteJson, readJsonSafe, unlinkSafe, sleepWithJitter, acquireFileLock, } from './atomic.js';
12
+ import { createAgentInfo } from './agent.js';
13
+ import { LockManager } from './lock-manager.js';
14
+ import { createDebugger } from '@eddacraft/anvil-core';
15
+ const debug = createDebugger('queue');
16
+ /**
17
+ * Queue Manager
18
+ *
19
+ * Manages fair queuing for resources across multiple agents.
20
+ */
21
+ export class QueueManager {
22
+ workspaceRoot;
23
+ config;
24
+ agent;
25
+ queueDir;
26
+ lockManager;
27
+ constructor(options) {
28
+ this.workspaceRoot = options.workspaceRoot;
29
+ this.config = {
30
+ ...getDefaultConcurrencyConfig(),
31
+ ...options.config,
32
+ };
33
+ this.agent = options.agentInfo ?? createAgentInfo();
34
+ this.queueDir = join(this.workspaceRoot, this.config.queueDir);
35
+ this.lockManager =
36
+ options.lockManager ??
37
+ new LockManager({
38
+ workspaceRoot: options.workspaceRoot,
39
+ config: options.config,
40
+ agentInfo: this.agent,
41
+ });
42
+ }
43
+ /**
44
+ * Get the agent ID
45
+ */
46
+ getAgentId() {
47
+ return this.agent.id;
48
+ }
49
+ /**
50
+ * Join a queue for a resource
51
+ */
52
+ async join(options) {
53
+ const { type, resource, priority = 100, reason, timeoutMs = this.config.queueTimeoutMs, } = options;
54
+ await this.ensureQueueDir();
55
+ const queuePath = this.getQueuePath(type, resource);
56
+ // Serialise access to the queue file to prevent lost updates
57
+ const lockPath = `${queuePath}.lock`;
58
+ const fileLock = await acquireFileLock(lockPath, {
59
+ timeout: Math.min(timeoutMs, 30_000),
60
+ retryInterval: 50,
61
+ });
62
+ if (!fileLock) {
63
+ throw new Error(`Timed out waiting for queue lock: ${type}:${resource}`);
64
+ }
65
+ try {
66
+ const queue = await this.loadQueue(queuePath, type, resource);
67
+ // Check if already in queue
68
+ const existingIndex = queue.entries.findIndex((e) => e.agentId === this.agent.id);
69
+ if (existingIndex !== -1) {
70
+ // Update existing entry
71
+ queue.entries[existingIndex] = {
72
+ ...queue.entries[existingIndex],
73
+ priority,
74
+ reason,
75
+ timeoutAt: new Date(Date.now() + timeoutMs).toISOString(),
76
+ };
77
+ this.sortQueue(queue);
78
+ queue.updatedAt = new Date().toISOString();
79
+ await atomicWriteJson(queuePath, queue);
80
+ const newPosition = queue.entries.findIndex((e) => e.agentId === this.agent.id) + 1;
81
+ debug(`Queue entry updated: ${type}:${resource} position=${newPosition}`);
82
+ return {
83
+ entryId: queue.entries[existingIndex].id,
84
+ position: newPosition,
85
+ alreadyQueued: true,
86
+ };
87
+ }
88
+ // Check queue size limit
89
+ if (queue.entries.length >= this.config.maxQueueSize) {
90
+ throw new Error(`Queue is full (max ${this.config.maxQueueSize} entries)`);
91
+ }
92
+ // Create new entry
93
+ const entry = {
94
+ id: randomUUID(),
95
+ agentId: this.agent.id,
96
+ agentType: this.agent.type,
97
+ lockType: type,
98
+ resource,
99
+ queuedAt: new Date().toISOString(),
100
+ priority,
101
+ timeoutAt: new Date(Date.now() + timeoutMs).toISOString(),
102
+ reason,
103
+ };
104
+ queue.entries.push(entry);
105
+ this.sortQueue(queue);
106
+ queue.updatedAt = new Date().toISOString();
107
+ await atomicWriteJson(queuePath, queue);
108
+ const position = queue.entries.findIndex((e) => e.id === entry.id) + 1;
109
+ debug(`Joined queue: ${type}:${resource} position=${position}`);
110
+ return {
111
+ entryId: entry.id,
112
+ position,
113
+ alreadyQueued: false,
114
+ };
115
+ }
116
+ finally {
117
+ await fileLock.release();
118
+ }
119
+ }
120
+ /**
121
+ * Leave a queue
122
+ */
123
+ async leave(type, resource) {
124
+ const queuePath = this.getQueuePath(type, resource);
125
+ const queue = await this.loadQueue(queuePath, type, resource);
126
+ const index = queue.entries.findIndex((e) => e.agentId === this.agent.id);
127
+ if (index === -1) {
128
+ return false;
129
+ }
130
+ queue.entries.splice(index, 1);
131
+ queue.updatedAt = new Date().toISOString();
132
+ if (queue.entries.length === 0) {
133
+ await unlinkSafe(queuePath);
134
+ }
135
+ else {
136
+ await atomicWriteJson(queuePath, queue);
137
+ }
138
+ debug(`Left queue: ${type}:${resource}`);
139
+ return true;
140
+ }
141
+ /**
142
+ * Get queue status
143
+ */
144
+ async getStatus(type, resource) {
145
+ const queuePath = this.getQueuePath(type, resource);
146
+ const queue = await this.loadQueue(queuePath, type, resource);
147
+ // Clean up timed-out entries
148
+ await this.cleanupTimedOut(queue, queuePath);
149
+ const yourEntry = queue.entries.find((e) => e.agentId === this.agent.id);
150
+ const yourPosition = yourEntry
151
+ ? queue.entries.findIndex((e) => e.agentId === this.agent.id) + 1
152
+ : undefined;
153
+ // Get current lock holder
154
+ const lockInfo = await this.lockManager.getLockInfo(type, resource);
155
+ return {
156
+ totalEntries: queue.entries.length,
157
+ yourPosition,
158
+ yourEntry,
159
+ currentHolder: lockInfo
160
+ ? {
161
+ agentId: lockInfo.agentId,
162
+ agentType: lockInfo.agentType,
163
+ acquiredAt: lockInfo.acquiredAt,
164
+ expiresAt: lockInfo.expiresAt,
165
+ }
166
+ : undefined,
167
+ };
168
+ }
169
+ /**
170
+ * Check if it's our turn (we're first in queue)
171
+ */
172
+ async isOurTurn(type, resource) {
173
+ const status = await this.getStatus(type, resource);
174
+ return status.yourPosition === 1;
175
+ }
176
+ /**
177
+ * Wait in queue until lock is acquired
178
+ *
179
+ * This is the main coordination primitive - joins queue, waits for turn,
180
+ * then acquires lock.
181
+ */
182
+ async waitForLock(options) {
183
+ const { type, resource, priority, reason, timeoutMs, maxWaitMs = this.config.queueTimeoutMs, pollIntervalMs = 500, onPositionChange, onLockAcquired, } = options;
184
+ const startTime = Date.now();
185
+ // First try to acquire lock directly (might be available)
186
+ const directResult = await this.lockManager.acquire({
187
+ type,
188
+ resource,
189
+ reason,
190
+ wait: false,
191
+ });
192
+ if (directResult.acquired) {
193
+ onLockAcquired?.();
194
+ return directResult;
195
+ }
196
+ // Join queue
197
+ const joinResult = await this.join({ type, resource, priority, reason, timeoutMs });
198
+ let lastPosition = joinResult.position;
199
+ debug(`Waiting in queue: ${type}:${resource} position=${lastPosition}`);
200
+ try {
201
+ while (Date.now() - startTime < maxWaitMs) {
202
+ // Check our position
203
+ const status = await this.getStatus(type, resource);
204
+ if (status.yourPosition !== lastPosition) {
205
+ lastPosition = status.yourPosition ?? lastPosition;
206
+ onPositionChange?.(lastPosition, status.totalEntries);
207
+ debug(`Queue position changed: ${type}:${resource} position=${lastPosition}`);
208
+ }
209
+ // If we're first, try to acquire lock
210
+ if (status.yourPosition === 1) {
211
+ const lockResult = await this.lockManager.acquire({
212
+ type,
213
+ resource,
214
+ reason,
215
+ wait: false,
216
+ });
217
+ if (lockResult.acquired) {
218
+ // Leave queue and return
219
+ await this.leave(type, resource);
220
+ onLockAcquired?.();
221
+ return lockResult;
222
+ }
223
+ // Lock still held, wait longer
224
+ }
225
+ // Wait before polling again
226
+ await sleepWithJitter(pollIntervalMs);
227
+ }
228
+ // Timeout
229
+ await this.leave(type, resource);
230
+ return {
231
+ acquired: false,
232
+ error: `Queue wait timed out after ${maxWaitMs}ms`,
233
+ queuePosition: lastPosition,
234
+ };
235
+ }
236
+ catch (error) {
237
+ // Cleanup on error
238
+ await this.leave(type, resource).catch(() => { });
239
+ throw error;
240
+ }
241
+ }
242
+ /**
243
+ * Get all queues in the workspace
244
+ */
245
+ async getAllQueues() {
246
+ const result = [];
247
+ try {
248
+ const files = await fs.readdir(this.queueDir);
249
+ for (const file of files) {
250
+ if (!file.endsWith('.json'))
251
+ continue;
252
+ const queuePath = join(this.queueDir, file);
253
+ const data = await readJsonSafe(queuePath);
254
+ if (data) {
255
+ const parseResult = QueueFileSchema.safeParse(data);
256
+ if (parseResult.success) {
257
+ result.push({
258
+ type: parseResult.data.lockType,
259
+ resource: parseResult.data.resource,
260
+ entries: parseResult.data.entries.length,
261
+ });
262
+ }
263
+ }
264
+ }
265
+ }
266
+ catch (error) {
267
+ if (error.code !== 'ENOENT') {
268
+ debug('Failed to list queues:', error);
269
+ }
270
+ }
271
+ return result;
272
+ }
273
+ /**
274
+ * Cleanup all timed-out entries across all queues
275
+ */
276
+ async cleanupAllTimedOut() {
277
+ let cleaned = 0;
278
+ try {
279
+ const files = await fs.readdir(this.queueDir);
280
+ for (const file of files) {
281
+ if (!file.endsWith('.json'))
282
+ continue;
283
+ const queuePath = join(this.queueDir, file);
284
+ const data = await readJsonSafe(queuePath);
285
+ if (data) {
286
+ const parseResult = QueueFileSchema.safeParse(data);
287
+ if (parseResult.success) {
288
+ cleaned += await this.cleanupTimedOut(parseResult.data, queuePath);
289
+ }
290
+ }
291
+ }
292
+ }
293
+ catch (error) {
294
+ if (error.code !== 'ENOENT') {
295
+ debug('Cleanup failed:', error);
296
+ }
297
+ }
298
+ return cleaned;
299
+ }
300
+ // ============================================================================
301
+ // Private Methods
302
+ // ============================================================================
303
+ /**
304
+ * Load queue from file
305
+ */
306
+ async loadQueue(queuePath, type, resource) {
307
+ const data = await readJsonSafe(queuePath);
308
+ if (data) {
309
+ const result = QueueFileSchema.safeParse(data);
310
+ if (result.success) {
311
+ return result.data;
312
+ }
313
+ debug('Invalid queue file schema:', result.error);
314
+ }
315
+ return {
316
+ version: '1.0.0',
317
+ resource,
318
+ lockType: type,
319
+ updatedAt: new Date().toISOString(),
320
+ entries: [],
321
+ };
322
+ }
323
+ /**
324
+ * Sort queue entries by priority then by queued time
325
+ */
326
+ sortQueue(queue) {
327
+ queue.entries.sort((a, b) => {
328
+ // Lower priority number = higher priority
329
+ if (a.priority !== b.priority) {
330
+ return a.priority - b.priority;
331
+ }
332
+ // Earlier queued = higher priority
333
+ return new Date(a.queuedAt).getTime() - new Date(b.queuedAt).getTime();
334
+ });
335
+ }
336
+ /**
337
+ * Cleanup timed-out entries
338
+ */
339
+ async cleanupTimedOut(queue, queuePath) {
340
+ const now = Date.now();
341
+ const before = queue.entries.length;
342
+ queue.entries = queue.entries.filter((entry) => {
343
+ const timeoutAt = new Date(entry.timeoutAt).getTime();
344
+ return now < timeoutAt;
345
+ });
346
+ const removed = before - queue.entries.length;
347
+ if (removed > 0) {
348
+ if (queue.entries.length === 0) {
349
+ await unlinkSafe(queuePath);
350
+ }
351
+ else {
352
+ queue.updatedAt = new Date().toISOString();
353
+ await atomicWriteJson(queuePath, queue);
354
+ }
355
+ debug(`Cleaned up ${removed} timed-out queue entries`);
356
+ }
357
+ return removed;
358
+ }
359
+ /**
360
+ * Get queue file path
361
+ */
362
+ getQueuePath(type, resource) {
363
+ const key = `${type}:${resource}`;
364
+ const hash = createHash('sha256').update(key).digest('hex').slice(0, 16);
365
+ return join(this.queueDir, `${hash}.json`);
366
+ }
367
+ /**
368
+ * Ensure queue directory exists
369
+ */
370
+ async ensureQueueDir() {
371
+ await fs.mkdir(this.queueDir, { recursive: true });
372
+ }
373
+ }
374
+ /**
375
+ * Create a queue manager
376
+ */
377
+ export function createQueueManager(options) {
378
+ return new QueueManager(options);
379
+ }
380
+ // ============================================================================
381
+ // High-Level Coordination Primitives
382
+ // ============================================================================
383
+ /**
384
+ * Coordinated execution with fair queuing
385
+ *
386
+ * This is the primary way to execute code that requires exclusive access
387
+ * to a resource in a multi-agent environment.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * const result = await coordinatedExecution(
392
+ * queueManager,
393
+ * { type: 'action', resource: 'gate', reason: 'Running quality gates' },
394
+ * async () => {
395
+ * return await runGates();
396
+ * }
397
+ * );
398
+ * ```
399
+ */
400
+ export async function coordinatedExecution(manager, options, fn) {
401
+ const lockResult = await manager.waitForLock(options);
402
+ if (!lockResult.acquired) {
403
+ throw new Error(`Failed to acquire lock: ${lockResult.error}`);
404
+ }
405
+ try {
406
+ return await fn();
407
+ }
408
+ finally {
409
+ await manager['lockManager'].release(options.type, options.resource);
410
+ }
411
+ }
412
+ /**
413
+ * Execute function with coordination, allowing limited concurrency
414
+ *
415
+ * Useful when you want to allow some parallel execution but limit the
416
+ * total number of concurrent agents.
417
+ */
418
+ export async function withConcurrencyLimit(manager, type, resource, maxConcurrent, fn) {
419
+ // Use resource slots for concurrency limiting
420
+ for (let slot = 0; slot < maxConcurrent; slot++) {
421
+ const slotResource = `${resource}:slot-${slot}`;
422
+ const result = await manager.waitForLock({
423
+ type,
424
+ resource: slotResource,
425
+ maxWaitMs: 1000, // Quick check per slot
426
+ });
427
+ if (result.acquired) {
428
+ try {
429
+ return await fn();
430
+ }
431
+ finally {
432
+ await manager['lockManager'].release(type, slotResource);
433
+ }
434
+ }
435
+ }
436
+ // All slots taken, wait for any slot
437
+ return coordinatedExecution(manager, {
438
+ type,
439
+ resource: `${resource}:slot-0`, // Wait for first slot
440
+ maxWaitMs: manager['config'].queueTimeoutMs,
441
+ }, fn);
442
+ }