@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,440 @@
1
+ /**
2
+ * Agent Manager
3
+ *
4
+ * Handles agent identification, registration, and heartbeat management
5
+ * for multi-agent coordination in Anvil.
6
+ */
7
+ import { promises as fs } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ import { AgentRegistrySchema, AgentInfoSchema, getDefaultConcurrencyConfig, } from './types.js';
10
+ import { atomicWriteJson, readJsonSafe } from './atomic.js';
11
+ import { createDebugger } from '@eddacraft/anvil-core';
12
+ const debug = createDebugger('agent');
13
+ // ============================================================================
14
+ // Agent ID Detection
15
+ // ============================================================================
16
+ /**
17
+ * Environment variables for agent identification
18
+ */
19
+ const AGENT_ENV_VARS = {
20
+ // Explicit Anvil agent ID
21
+ ANVIL_AGENT_ID: 'ANVIL_AGENT_ID',
22
+ ANVIL_AGENT_TYPE: 'ANVIL_AGENT_TYPE',
23
+ ANVIL_AGENT_NAME: 'ANVIL_AGENT_NAME',
24
+ ANVIL_SESSION_ID: 'ANVIL_SESSION_ID',
25
+ // Claude Code specific
26
+ CLAUDE_SESSION_ID: 'CLAUDE_SESSION_ID',
27
+ CLAUDE_CODE_SESSION: 'CLAUDE_CODE_SESSION',
28
+ // Cursor specific
29
+ CURSOR_SESSION_ID: 'CURSOR_SESSION_ID',
30
+ // General AI tool indicators
31
+ AI_TOOL: 'AI_TOOL',
32
+ EDITOR_PID: 'EDITOR_PID',
33
+ // CI indicators
34
+ CI: 'CI',
35
+ GITHUB_ACTIONS: 'GITHUB_ACTIONS',
36
+ GITLAB_CI: 'GITLAB_CI',
37
+ CIRCLECI: 'CIRCLECI',
38
+ JENKINS_URL: 'JENKINS_URL',
39
+ };
40
+ /**
41
+ * Detect agent type from environment
42
+ */
43
+ export function detectAgentType() {
44
+ const env = process.env;
45
+ // Explicit type setting
46
+ if (env[AGENT_ENV_VARS.ANVIL_AGENT_TYPE]) {
47
+ const type = env[AGENT_ENV_VARS.ANVIL_AGENT_TYPE]?.toLowerCase();
48
+ if (type &&
49
+ ['claude', 'cursor', 'copilot', 'aider', 'continue', 'codeium', 'human', 'ci'].includes(type)) {
50
+ return type;
51
+ }
52
+ }
53
+ // CI detection
54
+ if (env[AGENT_ENV_VARS.CI] ||
55
+ env[AGENT_ENV_VARS.GITHUB_ACTIONS] ||
56
+ env[AGENT_ENV_VARS.GITLAB_CI] ||
57
+ env[AGENT_ENV_VARS.CIRCLECI] ||
58
+ env[AGENT_ENV_VARS.JENKINS_URL]) {
59
+ return 'ci';
60
+ }
61
+ // Claude Code detection
62
+ if (env[AGENT_ENV_VARS.CLAUDE_SESSION_ID] || env[AGENT_ENV_VARS.CLAUDE_CODE_SESSION]) {
63
+ return 'claude';
64
+ }
65
+ // Cursor detection
66
+ if (env[AGENT_ENV_VARS.CURSOR_SESSION_ID]) {
67
+ return 'cursor';
68
+ }
69
+ // Explicit AI tool setting
70
+ if (env[AGENT_ENV_VARS.AI_TOOL]) {
71
+ const tool = env[AGENT_ENV_VARS.AI_TOOL]?.toLowerCase();
72
+ if (tool === 'aider')
73
+ return 'aider';
74
+ if (tool === 'continue')
75
+ return 'continue';
76
+ if (tool === 'codeium')
77
+ return 'codeium';
78
+ if (tool === 'copilot')
79
+ return 'copilot';
80
+ }
81
+ // Check if running interactively (likely human)
82
+ if (process.stdin.isTTY && !env[AGENT_ENV_VARS.AI_TOOL]) {
83
+ return 'human';
84
+ }
85
+ return 'unknown';
86
+ }
87
+ /**
88
+ * Get or generate agent ID
89
+ */
90
+ export function getAgentId() {
91
+ // Check explicit agent ID
92
+ const explicitId = process.env[AGENT_ENV_VARS.ANVIL_AGENT_ID];
93
+ if (explicitId) {
94
+ return explicitId;
95
+ }
96
+ // Use session IDs if available
97
+ if (process.env[AGENT_ENV_VARS.ANVIL_SESSION_ID]) {
98
+ return `session-${process.env[AGENT_ENV_VARS.ANVIL_SESSION_ID]}`;
99
+ }
100
+ if (process.env[AGENT_ENV_VARS.CLAUDE_SESSION_ID]) {
101
+ return `claude-${process.env[AGENT_ENV_VARS.CLAUDE_SESSION_ID]}`;
102
+ }
103
+ if (process.env[AGENT_ENV_VARS.CURSOR_SESSION_ID]) {
104
+ return `cursor-${process.env[AGENT_ENV_VARS.CURSOR_SESSION_ID]}`;
105
+ }
106
+ // CI-specific IDs
107
+ if (process.env['GITHUB_RUN_ID']) {
108
+ return `gh-${process.env['GITHUB_RUN_ID']}-${process.env['GITHUB_RUN_ATTEMPT'] || '1'}`;
109
+ }
110
+ if (process.env['CI_JOB_ID']) {
111
+ return `ci-${process.env['CI_JOB_ID']}`;
112
+ }
113
+ // Generate based on process
114
+ return `proc-${process.pid}-${Date.now().toString(36)}`;
115
+ }
116
+ /**
117
+ * Get session ID if available
118
+ */
119
+ export function getSessionId() {
120
+ return (process.env[AGENT_ENV_VARS.ANVIL_SESSION_ID] ||
121
+ process.env[AGENT_ENV_VARS.CLAUDE_SESSION_ID] ||
122
+ process.env[AGENT_ENV_VARS.CURSOR_SESSION_ID] ||
123
+ undefined);
124
+ }
125
+ /**
126
+ * Get agent name from environment or generate one
127
+ */
128
+ export function getAgentName() {
129
+ const explicitName = process.env[AGENT_ENV_VARS.ANVIL_AGENT_NAME];
130
+ if (explicitName) {
131
+ return explicitName;
132
+ }
133
+ const type = detectAgentType();
134
+ const pid = process.pid;
135
+ switch (type) {
136
+ case 'claude':
137
+ return `Claude Code (${pid})`;
138
+ case 'cursor':
139
+ return `Cursor AI (${pid})`;
140
+ case 'copilot':
141
+ return `GitHub Copilot (${pid})`;
142
+ case 'aider':
143
+ return `Aider (${pid})`;
144
+ case 'continue':
145
+ return `Continue (${pid})`;
146
+ case 'codeium':
147
+ return `Codeium (${pid})`;
148
+ case 'human':
149
+ return `Human Developer (${pid})`;
150
+ case 'ci':
151
+ return `CI Runner (${pid})`;
152
+ default:
153
+ return `Agent (${pid})`;
154
+ }
155
+ }
156
+ /**
157
+ * Create agent info from environment
158
+ */
159
+ export function createAgentInfo(overrides) {
160
+ return AgentInfoSchema.parse({
161
+ id: getAgentId(),
162
+ type: detectAgentType(),
163
+ pid: process.pid,
164
+ name: getAgentName(),
165
+ sessionId: getSessionId(),
166
+ ...overrides,
167
+ });
168
+ }
169
+ /**
170
+ * Agent Manager
171
+ *
172
+ * Manages agent registration, heartbeat, and lifecycle in a multi-agent environment.
173
+ */
174
+ export class AgentManager {
175
+ workspaceRoot;
176
+ config;
177
+ agent;
178
+ registryPath;
179
+ heartbeatTimer = null;
180
+ isRegistered = false;
181
+ constructor(options) {
182
+ this.workspaceRoot = options.workspaceRoot;
183
+ this.config = {
184
+ ...getDefaultConcurrencyConfig(),
185
+ ...options.config,
186
+ };
187
+ this.agent = options.agentInfo ?? createAgentInfo();
188
+ this.registryPath = join(this.workspaceRoot, this.config.registryPath);
189
+ }
190
+ /**
191
+ * Check if agent is registered
192
+ */
193
+ get registered() {
194
+ return this.isRegistered;
195
+ }
196
+ /**
197
+ * Get the current agent info
198
+ */
199
+ getAgent() {
200
+ return { ...this.agent };
201
+ }
202
+ /**
203
+ * Get the agent ID
204
+ */
205
+ getAgentId() {
206
+ return this.agent.id;
207
+ }
208
+ /**
209
+ * Register this agent
210
+ */
211
+ async register(operation) {
212
+ await this.ensureDir();
213
+ const registry = await this.loadRegistry();
214
+ const now = new Date().toISOString();
215
+ const registration = {
216
+ agent: this.agent,
217
+ registeredAt: now,
218
+ lastHeartbeat: now,
219
+ heartbeatCount: 0,
220
+ state: 'active',
221
+ currentOperation: operation,
222
+ workspaceRoot: this.workspaceRoot,
223
+ };
224
+ registry.agents[this.agent.id] = registration;
225
+ registry.updatedAt = now;
226
+ await this.saveRegistry(registry);
227
+ this.isRegistered = true;
228
+ debug(`Agent registered: ${this.agent.id} (${this.agent.type})`);
229
+ }
230
+ /**
231
+ * Start heartbeat timer
232
+ */
233
+ startHeartbeat() {
234
+ if (this.heartbeatTimer) {
235
+ return;
236
+ }
237
+ this.heartbeatTimer = setInterval(async () => {
238
+ try {
239
+ await this.heartbeat();
240
+ }
241
+ catch (error) {
242
+ debug('Heartbeat failed:', error);
243
+ }
244
+ }, this.config.heartbeatIntervalMs);
245
+ // Don't block process exit for heartbeat
246
+ this.heartbeatTimer.unref();
247
+ debug(`Heartbeat started: interval=${this.config.heartbeatIntervalMs}ms`);
248
+ }
249
+ /**
250
+ * Stop heartbeat timer
251
+ */
252
+ stopHeartbeat() {
253
+ if (this.heartbeatTimer) {
254
+ clearInterval(this.heartbeatTimer);
255
+ this.heartbeatTimer = null;
256
+ debug('Heartbeat stopped');
257
+ }
258
+ }
259
+ /**
260
+ * Send a heartbeat
261
+ */
262
+ async heartbeat(operation) {
263
+ const registry = await this.loadRegistry();
264
+ const registration = registry.agents[this.agent.id];
265
+ if (!registration) {
266
+ // Re-register if not in registry
267
+ await this.register(operation);
268
+ return;
269
+ }
270
+ const now = new Date().toISOString();
271
+ registration.lastHeartbeat = now;
272
+ registration.heartbeatCount++;
273
+ registration.state = 'active';
274
+ if (operation !== undefined) {
275
+ registration.currentOperation = operation;
276
+ }
277
+ registry.updatedAt = now;
278
+ await this.saveRegistry(registry);
279
+ debug(`Heartbeat sent: ${this.agent.id} (count=${registration.heartbeatCount})`);
280
+ }
281
+ /**
282
+ * Update current operation
283
+ */
284
+ async setOperation(operation) {
285
+ const registry = await this.loadRegistry();
286
+ const registration = registry.agents[this.agent.id];
287
+ if (registration) {
288
+ registration.currentOperation = operation;
289
+ registration.lastHeartbeat = new Date().toISOString();
290
+ registry.updatedAt = new Date().toISOString();
291
+ await this.saveRegistry(registry);
292
+ }
293
+ }
294
+ /**
295
+ * Unregister this agent
296
+ */
297
+ async unregister() {
298
+ this.stopHeartbeat();
299
+ const registry = await this.loadRegistry();
300
+ if (registry.agents[this.agent.id]) {
301
+ registry.agents[this.agent.id].state = 'terminated';
302
+ registry.updatedAt = new Date().toISOString();
303
+ await this.saveRegistry(registry);
304
+ }
305
+ this.isRegistered = false;
306
+ debug(`Agent unregistered: ${this.agent.id}`);
307
+ }
308
+ /**
309
+ * Get all registered agents
310
+ */
311
+ async getAllAgents() {
312
+ const registry = await this.loadRegistry();
313
+ return Object.values(registry.agents);
314
+ }
315
+ /**
316
+ * Get active agents
317
+ */
318
+ async getActiveAgents() {
319
+ const registry = await this.loadRegistry();
320
+ const now = Date.now();
321
+ const staleThreshold = this.config.staleThresholdMs;
322
+ return Object.values(registry.agents).filter((reg) => {
323
+ const lastHeartbeat = new Date(reg.lastHeartbeat).getTime();
324
+ const isStale = now - lastHeartbeat > staleThreshold;
325
+ return reg.state === 'active' && !isStale;
326
+ });
327
+ }
328
+ /**
329
+ * Cleanup stale agents
330
+ */
331
+ async cleanupStaleAgents() {
332
+ const registry = await this.loadRegistry();
333
+ const now = Date.now();
334
+ const staleThreshold = this.config.staleThresholdMs;
335
+ const staleAgents = [];
336
+ for (const [id, reg] of Object.entries(registry.agents)) {
337
+ const lastHeartbeat = new Date(reg.lastHeartbeat).getTime();
338
+ const isStale = now - lastHeartbeat > staleThreshold;
339
+ if (isStale && reg.state === 'active') {
340
+ reg.state = 'stale';
341
+ staleAgents.push(id);
342
+ debug(`Agent marked stale: ${id}`);
343
+ }
344
+ }
345
+ if (staleAgents.length > 0) {
346
+ registry.updatedAt = new Date().toISOString();
347
+ await this.saveRegistry(registry);
348
+ }
349
+ return staleAgents;
350
+ }
351
+ /**
352
+ * Check if an agent is stale
353
+ */
354
+ async isAgentStale(agentId) {
355
+ const registry = await this.loadRegistry();
356
+ const registration = registry.agents[agentId];
357
+ if (!registration) {
358
+ return true;
359
+ }
360
+ if (registration.state === 'stale' || registration.state === 'terminated') {
361
+ return true;
362
+ }
363
+ const now = Date.now();
364
+ const lastHeartbeat = new Date(registration.lastHeartbeat).getTime();
365
+ return now - lastHeartbeat > this.config.staleThresholdMs;
366
+ }
367
+ /**
368
+ * Check if a process is still running
369
+ */
370
+ isProcessRunning(pid) {
371
+ try {
372
+ // Sending signal 0 doesn't actually send a signal but checks if process exists
373
+ process.kill(pid, 0);
374
+ return true;
375
+ }
376
+ catch {
377
+ return false;
378
+ }
379
+ }
380
+ /**
381
+ * Get git user for commit trailers
382
+ */
383
+ getGitAgentTrailer() {
384
+ return `Anvil-Agent: ${this.agent.id} (${this.agent.type})`;
385
+ }
386
+ /**
387
+ * Load registry from file
388
+ */
389
+ async loadRegistry() {
390
+ const data = await readJsonSafe(this.registryPath);
391
+ if (data) {
392
+ const result = AgentRegistrySchema.safeParse(data);
393
+ if (result.success) {
394
+ return result.data;
395
+ }
396
+ debug('Invalid registry schema, creating new:', result.error);
397
+ }
398
+ return {
399
+ version: '1.0.0',
400
+ updatedAt: new Date().toISOString(),
401
+ agents: {},
402
+ };
403
+ }
404
+ /**
405
+ * Save registry to file
406
+ */
407
+ async saveRegistry(registry) {
408
+ await atomicWriteJson(this.registryPath, registry);
409
+ }
410
+ /**
411
+ * Ensure registry directory exists
412
+ */
413
+ async ensureDir() {
414
+ const dir = dirname(this.registryPath);
415
+ await fs.mkdir(dir, { recursive: true });
416
+ }
417
+ }
418
+ /**
419
+ * Create an agent manager
420
+ */
421
+ export function createAgentManager(options) {
422
+ return new AgentManager(options);
423
+ }
424
+ /**
425
+ * Global agent manager singleton (optional usage pattern)
426
+ */
427
+ let globalAgentManager = null;
428
+ /**
429
+ * Initialize global agent manager
430
+ */
431
+ export function initializeGlobalAgent(options) {
432
+ globalAgentManager = createAgentManager(options);
433
+ return globalAgentManager;
434
+ }
435
+ /**
436
+ * Get global agent manager
437
+ */
438
+ export function getGlobalAgent() {
439
+ return globalAgentManager;
440
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Atomic File Operations
3
+ *
4
+ * Provides atomic read/write operations for JSON files to prevent
5
+ * corruption in multi-agent/multi-process scenarios.
6
+ *
7
+ * Uses the atomic write pattern: write to temp file, then rename.
8
+ * This ensures that either the old or new content is visible, never partial.
9
+ */
10
+ /**
11
+ * Atomic write options
12
+ */
13
+ export interface AtomicWriteOptions {
14
+ /** File mode (default: 0o644) */
15
+ mode?: number;
16
+ /** Retry count for rename conflicts (default: 3) */
17
+ retries?: number;
18
+ /** Create parent directories if needed (default: true) */
19
+ createDirs?: boolean;
20
+ }
21
+ /**
22
+ * Write JSON atomically
23
+ *
24
+ * 1. Write content to a temp file in the same directory
25
+ * 2. Rename temp file to target (atomic on most filesystems)
26
+ * 3. Clean up temp file on error
27
+ */
28
+ export declare function atomicWriteJson(filePath: string, data: unknown, options?: AtomicWriteOptions): Promise<void>;
29
+ /**
30
+ * Write text atomically
31
+ */
32
+ export declare function atomicWriteText(filePath: string, content: string, options?: AtomicWriteOptions): Promise<void>;
33
+ /**
34
+ * Read JSON safely (returns null if file doesn't exist or is invalid)
35
+ */
36
+ export declare function readJsonSafe<T = unknown>(filePath: string): Promise<T | null>;
37
+ /**
38
+ * Read JSON with retries (for handling transient lock conflicts)
39
+ */
40
+ export declare function readJsonWithRetry<T = unknown>(filePath: string, retries?: number, delayMs?: number): Promise<T | null>;
41
+ /**
42
+ * Atomic file lock using a lock file
43
+ *
44
+ * Uses O_EXCL flag for atomic creation - only succeeds if file doesn't exist.
45
+ * This provides a simple but robust mutual exclusion mechanism.
46
+ */
47
+ export interface FileLockOptions {
48
+ /** Maximum time to wait for lock (ms) */
49
+ timeout?: number;
50
+ /** Retry interval (ms) */
51
+ retryInterval?: number;
52
+ /** Lock file content */
53
+ content?: string;
54
+ }
55
+ export interface FileLockHandle {
56
+ /** Path to the lock file */
57
+ path: string;
58
+ /** Release the lock */
59
+ release: () => Promise<void>;
60
+ }
61
+ /**
62
+ * Acquire a file lock (blocks until acquired or timeout)
63
+ */
64
+ export declare function acquireFileLock(lockPath: string, options?: FileLockOptions): Promise<FileLockHandle | null>;
65
+ /**
66
+ * Try to acquire a file lock (non-blocking)
67
+ */
68
+ export declare function tryAcquireFileLock(lockPath: string, content?: string): Promise<FileLockHandle | null>;
69
+ /**
70
+ * Check if a lock file exists
71
+ */
72
+ export declare function isLocked(lockPath: string): Promise<boolean>;
73
+ /**
74
+ * Force release a lock (use with caution)
75
+ */
76
+ export declare function forceReleaseLock(lockPath: string): Promise<boolean>;
77
+ /**
78
+ * Delete file if it exists (no error if missing)
79
+ */
80
+ export declare function unlinkSafe(filePath: string): Promise<boolean>;
81
+ /**
82
+ * Check if file exists
83
+ */
84
+ export declare function fileExists(filePath: string): Promise<boolean>;
85
+ /**
86
+ * Get file modification time
87
+ */
88
+ export declare function getFileMtime(filePath: string): Promise<Date | null>;
89
+ /**
90
+ * Sleep with jitter (to prevent thundering herd)
91
+ */
92
+ export declare function sleepWithJitter(baseMs: number, jitterPercent?: number): Promise<void>;
93
+ //# sourceMappingURL=atomic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../../src/concurrency/atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CA4Cf;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAmCf;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAWnF;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EACjD,QAAQ,EAAE,MAAM,EAChB,OAAO,SAAI,EACX,OAAO,SAAK,GACX,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAwBnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,0BAA0B;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,wBAAwB;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IAEb,uBAAuB;IACvB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAsChC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,SAAK,GACX,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA4BhC;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOjE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAWzE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUnE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAOzE;AASD;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF"}