@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.
- package/LICENSE +14 -0
- package/dist/cache/cache-key.d.ts +45 -0
- package/dist/cache/cache-key.d.ts.map +1 -0
- package/dist/cache/cache-key.js +135 -0
- package/dist/cache/index.d.ts +27 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +38 -0
- package/dist/cache/providers/file-cache.d.ts +63 -0
- package/dist/cache/providers/file-cache.d.ts.map +1 -0
- package/dist/cache/providers/file-cache.js +369 -0
- package/dist/cache/providers/memory-cache.d.ts +52 -0
- package/dist/cache/providers/memory-cache.d.ts.map +1 -0
- package/dist/cache/providers/memory-cache.js +197 -0
- package/dist/cache/providers/null-cache.d.ts +26 -0
- package/dist/cache/providers/null-cache.d.ts.map +1 -0
- package/dist/cache/providers/null-cache.js +50 -0
- package/dist/cache/types.d.ts +114 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +4 -0
- package/dist/concurrency/agent.d.ts +137 -0
- package/dist/concurrency/agent.d.ts.map +1 -0
- package/dist/concurrency/agent.js +440 -0
- package/dist/concurrency/atomic.d.ts +93 -0
- package/dist/concurrency/atomic.d.ts.map +1 -0
- package/dist/concurrency/atomic.js +281 -0
- package/dist/concurrency/git-agent.d.ts +114 -0
- package/dist/concurrency/git-agent.d.ts.map +1 -0
- package/dist/concurrency/git-agent.js +313 -0
- package/dist/concurrency/index.d.ts +95 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +127 -0
- package/dist/concurrency/lock-manager.d.ts +170 -0
- package/dist/concurrency/lock-manager.d.ts.map +1 -0
- package/dist/concurrency/lock-manager.js +525 -0
- package/dist/concurrency/queue-manager.d.ts +166 -0
- package/dist/concurrency/queue-manager.d.ts.map +1 -0
- package/dist/concurrency/queue-manager.js +442 -0
- package/dist/concurrency/types.d.ts +382 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +204 -0
- package/dist/export/constraint-collector.d.ts +175 -0
- package/dist/export/constraint-collector.d.ts.map +1 -0
- package/dist/export/constraint-collector.js +203 -0
- package/dist/export/formatters/llms-txt-formatter.d.ts +89 -0
- package/dist/export/formatters/llms-txt-formatter.d.ts.map +1 -0
- package/dist/export/formatters/llms-txt-formatter.js +249 -0
- package/dist/export/formatters/mcp-resource-formatter.d.ts +186 -0
- package/dist/export/formatters/mcp-resource-formatter.d.ts.map +1 -0
- package/dist/export/formatters/mcp-resource-formatter.js +139 -0
- package/dist/export/formatters/prompt-formatter.d.ts +83 -0
- package/dist/export/formatters/prompt-formatter.d.ts.map +1 -0
- package/dist/export/formatters/prompt-formatter.js +256 -0
- package/dist/export/index.d.ts +10 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +9 -0
- package/dist/gate/check.interface.d.ts +15 -0
- package/dist/gate/check.interface.d.ts.map +1 -0
- package/dist/gate/check.interface.js +18 -0
- package/dist/gate/checks/antipattern.check.d.ts +27 -0
- package/dist/gate/checks/antipattern.check.d.ts.map +1 -0
- package/dist/gate/checks/antipattern.check.js +140 -0
- package/dist/gate/checks/architecture/circular-detector.d.ts +33 -0
- package/dist/gate/checks/architecture/circular-detector.d.ts.map +1 -0
- package/dist/gate/checks/architecture/circular-detector.js +71 -0
- package/dist/gate/checks/architecture/dependency-analyzer.d.ts +81 -0
- package/dist/gate/checks/architecture/dependency-analyzer.d.ts.map +1 -0
- package/dist/gate/checks/architecture/dependency-analyzer.js +136 -0
- package/dist/gate/checks/architecture/layer-validator.d.ts +75 -0
- package/dist/gate/checks/architecture/layer-validator.d.ts.map +1 -0
- package/dist/gate/checks/architecture/layer-validator.js +193 -0
- package/dist/gate/checks/architecture.check.d.ts +56 -0
- package/dist/gate/checks/architecture.check.d.ts.map +1 -0
- package/dist/gate/checks/architecture.check.js +394 -0
- package/dist/gate/checks/command-safety.check.d.ts +12 -0
- package/dist/gate/checks/command-safety.check.d.ts.map +1 -0
- package/dist/gate/checks/command-safety.check.js +230 -0
- package/dist/gate/checks/coverage.check.d.ts +9 -0
- package/dist/gate/checks/coverage.check.d.ts.map +1 -0
- package/dist/gate/checks/coverage.check.js +81 -0
- package/dist/gate/checks/dependency.check.d.ts +17 -0
- package/dist/gate/checks/dependency.check.d.ts.map +1 -0
- package/dist/gate/checks/dependency.check.js +342 -0
- package/dist/gate/checks/eslint.check.d.ts +14 -0
- package/dist/gate/checks/eslint.check.d.ts.map +1 -0
- package/dist/gate/checks/eslint.check.js +79 -0
- package/dist/gate/checks/policy.check.d.ts +78 -0
- package/dist/gate/checks/policy.check.d.ts.map +1 -0
- package/dist/gate/checks/policy.check.js +457 -0
- package/dist/gate/checks/secret/entropy-detector.d.ts +44 -0
- package/dist/gate/checks/secret/entropy-detector.d.ts.map +1 -0
- package/dist/gate/checks/secret/entropy-detector.js +76 -0
- package/dist/gate/checks/secret/git-scanner.d.ts +36 -0
- package/dist/gate/checks/secret/git-scanner.d.ts.map +1 -0
- package/dist/gate/checks/secret/git-scanner.js +90 -0
- package/dist/gate/checks/secret/secret-patterns.d.ts +42 -0
- package/dist/gate/checks/secret/secret-patterns.d.ts.map +1 -0
- package/dist/gate/checks/secret/secret-patterns.js +137 -0
- package/dist/gate/checks/secret.check.d.ts +56 -0
- package/dist/gate/checks/secret.check.d.ts.map +1 -0
- package/dist/gate/checks/secret.check.js +245 -0
- package/dist/gate/config/command-safety-config.d.ts +5 -0
- package/dist/gate/config/command-safety-config.d.ts.map +1 -0
- package/dist/gate/config/command-safety-config.js +69 -0
- package/dist/gate/config/index.d.ts +2 -0
- package/dist/gate/config/index.d.ts.map +1 -0
- package/dist/gate/config/index.js +1 -0
- package/dist/gate/formatters/command-safety-formatter.d.ts +10 -0
- package/dist/gate/formatters/command-safety-formatter.d.ts.map +1 -0
- package/dist/gate/formatters/command-safety-formatter.js +64 -0
- package/dist/gate/formatters/index.d.ts +2 -0
- package/dist/gate/formatters/index.d.ts.map +1 -0
- package/dist/gate/formatters/index.js +1 -0
- package/dist/gate/gate-config.d.ts +44 -0
- package/dist/gate/gate-config.d.ts.map +1 -0
- package/dist/gate/gate-config.js +334 -0
- package/dist/gate/gate-runner.d.ts +160 -0
- package/dist/gate/gate-runner.d.ts.map +1 -0
- package/dist/gate/gate-runner.js +531 -0
- package/dist/gate/index.d.ts +20 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/index.js +14 -0
- package/dist/gate/parsers/command-parser.d.ts +18 -0
- package/dist/gate/parsers/command-parser.d.ts.map +1 -0
- package/dist/gate/parsers/command-parser.js +363 -0
- package/dist/gate/parsers/index.d.ts +2 -0
- package/dist/gate/parsers/index.d.ts.map +1 -0
- package/dist/gate/parsers/index.js +1 -0
- package/dist/gate/policy/index.d.ts +12 -0
- package/dist/gate/policy/index.d.ts.map +1 -0
- package/dist/gate/policy/index.js +10 -0
- package/dist/gate/rules/default-filesystem-rules.d.ts +3 -0
- package/dist/gate/rules/default-filesystem-rules.d.ts.map +1 -0
- package/dist/gate/rules/default-filesystem-rules.js +201 -0
- package/dist/gate/rules/default-git-rules.d.ts +3 -0
- package/dist/gate/rules/default-git-rules.d.ts.map +1 -0
- package/dist/gate/rules/default-git-rules.js +192 -0
- package/dist/gate/rules/index.d.ts +5 -0
- package/dist/gate/rules/index.d.ts.map +1 -0
- package/dist/gate/rules/index.js +3 -0
- package/dist/gate/rules/rule-matcher.d.ts +27 -0
- package/dist/gate/rules/rule-matcher.d.ts.map +1 -0
- package/dist/gate/rules/rule-matcher.js +228 -0
- package/dist/gate/rules/types.d.ts +250 -0
- package/dist/gate/rules/types.d.ts.map +1 -0
- package/dist/gate/rules/types.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/types/gate.types.d.ts +42 -0
- package/dist/types/gate.types.d.ts.map +1 -0
- package/dist/types/gate.types.js +94 -0
- package/dist/watch/debouncer.d.ts +90 -0
- package/dist/watch/debouncer.d.ts.map +1 -0
- package/dist/watch/debouncer.js +135 -0
- package/dist/watch/file-watcher.d.ts +73 -0
- package/dist/watch/file-watcher.d.ts.map +1 -0
- package/dist/watch/file-watcher.js +121 -0
- package/dist/watch/git-status.d.ts +98 -0
- package/dist/watch/git-status.d.ts.map +1 -0
- package/dist/watch/git-status.js +266 -0
- package/dist/watch/index.d.ts +16 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +15 -0
- package/dist/watch/orchestrator.d.ts +113 -0
- package/dist/watch/orchestrator.d.ts.map +1 -0
- package/dist/watch/orchestrator.js +409 -0
- package/dist/watch/types.d.ts +190 -0
- package/dist/watch/types.d.ts.map +1 -0
- package/dist/watch/types.js +76 -0
- 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"}
|