@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,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Agent Identification
|
|
3
|
+
*
|
|
4
|
+
* Utilities for identifying agents via git commit metadata.
|
|
5
|
+
* Supports reading and writing agent identification through commit trailers.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { createAgentInfo } from './agent.js';
|
|
10
|
+
import { createDebugger } from '@eddacraft/anvil-core';
|
|
11
|
+
const debug = createDebugger('git-agent');
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Trailer Constants
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Git trailer keys used for agent identification
|
|
17
|
+
*/
|
|
18
|
+
export const GIT_TRAILERS = {
|
|
19
|
+
/** Agent ID trailer */
|
|
20
|
+
AGENT_ID: 'Anvil-Agent-ID',
|
|
21
|
+
/** Agent type trailer */
|
|
22
|
+
AGENT_TYPE: 'Anvil-Agent-Type',
|
|
23
|
+
/** Session ID trailer */
|
|
24
|
+
SESSION_ID: 'Anvil-Session-ID',
|
|
25
|
+
/** Agent name trailer */
|
|
26
|
+
AGENT_NAME: 'Anvil-Agent-Name',
|
|
27
|
+
/** Co-authored-by for agent attribution */
|
|
28
|
+
CO_AUTHORED_BY: 'Co-authored-by',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Parse trailers from commit message
|
|
32
|
+
*/
|
|
33
|
+
export function parseCommitTrailers(commitMessage) {
|
|
34
|
+
const trailers = {};
|
|
35
|
+
const lines = commitMessage.split('\n');
|
|
36
|
+
// Trailers are at the end, after a blank line
|
|
37
|
+
let inTrailers = false;
|
|
38
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
39
|
+
const line = lines[i].trim();
|
|
40
|
+
if (line === '') {
|
|
41
|
+
if (inTrailers)
|
|
42
|
+
break;
|
|
43
|
+
inTrailers = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (inTrailers) {
|
|
47
|
+
const match = line.match(/^([A-Za-z][A-Za-z0-9-]*)\s*:\s*(.+)$/);
|
|
48
|
+
if (match) {
|
|
49
|
+
trailers[match[1]] = match[2];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return trailers;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract agent info from commit trailers
|
|
57
|
+
*/
|
|
58
|
+
export function extractAgentInfo(trailers) {
|
|
59
|
+
const agentId = trailers[GIT_TRAILERS.AGENT_ID];
|
|
60
|
+
const agentTypeStr = trailers[GIT_TRAILERS.AGENT_TYPE];
|
|
61
|
+
const sessionId = trailers[GIT_TRAILERS.SESSION_ID];
|
|
62
|
+
const agentName = trailers[GIT_TRAILERS.AGENT_NAME];
|
|
63
|
+
// Check for AI indicators
|
|
64
|
+
const isAiGenerated = !!agentId ||
|
|
65
|
+
!!agentTypeStr ||
|
|
66
|
+
Object.values(trailers).some((v) => v.toLowerCase().includes('ai') ||
|
|
67
|
+
v.toLowerCase().includes('claude') ||
|
|
68
|
+
v.toLowerCase().includes('copilot') ||
|
|
69
|
+
v.toLowerCase().includes('cursor'));
|
|
70
|
+
// Extract co-authors
|
|
71
|
+
const coAuthors = [];
|
|
72
|
+
for (const [key, value] of Object.entries(trailers)) {
|
|
73
|
+
if (key.toLowerCase() === 'co-authored-by') {
|
|
74
|
+
coAuthors.push(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
agentId,
|
|
79
|
+
agentType: agentTypeStr,
|
|
80
|
+
sessionId,
|
|
81
|
+
agentName,
|
|
82
|
+
isAiGenerated,
|
|
83
|
+
coAuthors,
|
|
84
|
+
trailers,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get agent info from a specific commit
|
|
89
|
+
*/
|
|
90
|
+
export function getCommitAgentInfo(commitRef, cwd) {
|
|
91
|
+
try {
|
|
92
|
+
const message = execFileSync('git', ['log', '-1', '--format=%B', commitRef], {
|
|
93
|
+
cwd,
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
+
});
|
|
97
|
+
const trailers = parseCommitTrailers(message);
|
|
98
|
+
return extractAgentInfo(trailers);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
debug(`Failed to get commit agent info for ${commitRef}:`, error);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get agent info from recent commits
|
|
107
|
+
*/
|
|
108
|
+
export function getRecentCommitsAgentInfo(count = 10, cwd) {
|
|
109
|
+
const results = [];
|
|
110
|
+
try {
|
|
111
|
+
const hashes = execFileSync('git', ['log', `-${count}`, '--format=%H'], {
|
|
112
|
+
cwd,
|
|
113
|
+
encoding: 'utf-8',
|
|
114
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
115
|
+
})
|
|
116
|
+
.trim()
|
|
117
|
+
.split('\n');
|
|
118
|
+
for (const hash of hashes) {
|
|
119
|
+
const info = getCommitAgentInfo(hash, cwd);
|
|
120
|
+
if (info) {
|
|
121
|
+
results.push({ hash, info });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
debug('Failed to get recent commits:', error);
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Format a commit message with agent identification trailers
|
|
132
|
+
*/
|
|
133
|
+
export function formatCommitWithAgent(options) {
|
|
134
|
+
const { message, agent = createAgentInfo(), includeCoAuthor = true, additionalTrailers = {}, } = options;
|
|
135
|
+
const trailers = [];
|
|
136
|
+
// Add agent trailers
|
|
137
|
+
trailers.push(`${GIT_TRAILERS.AGENT_ID}: ${agent.id}`);
|
|
138
|
+
trailers.push(`${GIT_TRAILERS.AGENT_TYPE}: ${agent.type}`);
|
|
139
|
+
if (agent.sessionId) {
|
|
140
|
+
trailers.push(`${GIT_TRAILERS.SESSION_ID}: ${agent.sessionId}`);
|
|
141
|
+
}
|
|
142
|
+
if (agent.name) {
|
|
143
|
+
trailers.push(`${GIT_TRAILERS.AGENT_NAME}: ${agent.name}`);
|
|
144
|
+
}
|
|
145
|
+
// Add co-authored-by for AI agents
|
|
146
|
+
if (includeCoAuthor && agent.type !== 'human') {
|
|
147
|
+
const coAuthorName = getAgentCoAuthorName(agent);
|
|
148
|
+
trailers.push(`${GIT_TRAILERS.CO_AUTHORED_BY}: ${coAuthorName}`);
|
|
149
|
+
}
|
|
150
|
+
// Add additional trailers
|
|
151
|
+
for (const [key, value] of Object.entries(additionalTrailers)) {
|
|
152
|
+
trailers.push(`${key}: ${value}`);
|
|
153
|
+
}
|
|
154
|
+
// Ensure message has proper spacing before trailers
|
|
155
|
+
const trimmedMessage = message.trimEnd();
|
|
156
|
+
const trailersBlock = trailers.join('\n');
|
|
157
|
+
// Check if message already has trailers
|
|
158
|
+
const existingTrailers = parseCommitTrailers(trimmedMessage);
|
|
159
|
+
const hasExistingTrailers = Object.keys(existingTrailers).length > 0;
|
|
160
|
+
if (hasExistingTrailers) {
|
|
161
|
+
// Append to existing trailers
|
|
162
|
+
return `${trimmedMessage}\n${trailersBlock}`;
|
|
163
|
+
}
|
|
164
|
+
// Add blank line before trailers
|
|
165
|
+
return `${trimmedMessage}\n\n${trailersBlock}`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get co-author name for an agent
|
|
169
|
+
*/
|
|
170
|
+
function getAgentCoAuthorName(agent) {
|
|
171
|
+
switch (agent.type) {
|
|
172
|
+
case 'claude':
|
|
173
|
+
return 'Claude <claude@anthropic.com>';
|
|
174
|
+
case 'cursor':
|
|
175
|
+
return 'Cursor AI <ai@cursor.sh>';
|
|
176
|
+
case 'copilot':
|
|
177
|
+
return 'GitHub Copilot <copilot@github.com>';
|
|
178
|
+
case 'aider':
|
|
179
|
+
return 'Aider <aider@aider.chat>';
|
|
180
|
+
case 'continue':
|
|
181
|
+
return 'Continue <ai@continue.dev>';
|
|
182
|
+
case 'codeium':
|
|
183
|
+
return 'Codeium <ai@codeium.com>';
|
|
184
|
+
default:
|
|
185
|
+
return `AI Agent (${agent.id}) <noreply@example.com>`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Git Hook Helpers
|
|
190
|
+
// ============================================================================
|
|
191
|
+
/**
|
|
192
|
+
* Prepare commit message hook helper
|
|
193
|
+
*
|
|
194
|
+
* Automatically adds agent trailers to commit messages.
|
|
195
|
+
* Can be used in a prepare-commit-msg hook.
|
|
196
|
+
*/
|
|
197
|
+
export function prepareCommitMsgHook(commitMsgFile, commitSource, _sha1) {
|
|
198
|
+
// Only modify non-merge, non-squash commits
|
|
199
|
+
if (commitSource === 'merge' || commitSource === 'squash') {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Read original message
|
|
203
|
+
const originalMessage = readFileSync(commitMsgFile, 'utf-8');
|
|
204
|
+
// Check if already has agent trailers
|
|
205
|
+
const trailers = parseCommitTrailers(originalMessage);
|
|
206
|
+
if (trailers[GIT_TRAILERS.AGENT_ID]) {
|
|
207
|
+
return; // Already has agent info
|
|
208
|
+
}
|
|
209
|
+
// Add agent info
|
|
210
|
+
const agent = createAgentInfo();
|
|
211
|
+
// Only add for AI agents
|
|
212
|
+
if (agent.type === 'human' || agent.type === 'unknown') {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const modifiedMessage = formatCommitWithAgent({
|
|
216
|
+
message: originalMessage,
|
|
217
|
+
agent,
|
|
218
|
+
});
|
|
219
|
+
writeFileSync(commitMsgFile, modifiedMessage);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Generate a git config command to set up the prepare-commit-msg hook
|
|
223
|
+
*/
|
|
224
|
+
export function getHookSetupCommand() {
|
|
225
|
+
return `
|
|
226
|
+
# Add to .git/hooks/prepare-commit-msg:
|
|
227
|
+
#!/bin/sh
|
|
228
|
+
COMMIT_MSG_FILE=$1
|
|
229
|
+
COMMIT_SOURCE=$2
|
|
230
|
+
SHA1=$3
|
|
231
|
+
|
|
232
|
+
# Run the prepare-commit-msg hook via the runtime module
|
|
233
|
+
if command -v node &> /dev/null; then
|
|
234
|
+
node -e "
|
|
235
|
+
import('${`@eddacraft/anvil-runtime/concurrency`}')
|
|
236
|
+
.then(m => m.prepareCommitMsgHook('$COMMIT_MSG_FILE', '$COMMIT_SOURCE', '$SHA1'))
|
|
237
|
+
.catch(() => {});
|
|
238
|
+
"
|
|
239
|
+
fi
|
|
240
|
+
`.trim();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get contribution summary by agent
|
|
244
|
+
*/
|
|
245
|
+
export function getAgentContributions(sinceRef, cwd) {
|
|
246
|
+
const contributions = new Map();
|
|
247
|
+
try {
|
|
248
|
+
const args = ['log'];
|
|
249
|
+
if (sinceRef)
|
|
250
|
+
args.push(`${sinceRef}..HEAD`);
|
|
251
|
+
args.push('--format=%H|%aI');
|
|
252
|
+
const output = execFileSync('git', args, {
|
|
253
|
+
cwd,
|
|
254
|
+
encoding: 'utf-8',
|
|
255
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
256
|
+
}).trim();
|
|
257
|
+
if (!output)
|
|
258
|
+
return contributions;
|
|
259
|
+
const lines = output.split('\n');
|
|
260
|
+
for (const line of lines) {
|
|
261
|
+
const [hash, timestamp] = line.split('|');
|
|
262
|
+
const info = getCommitAgentInfo(hash, cwd);
|
|
263
|
+
if (!info?.agentId)
|
|
264
|
+
continue;
|
|
265
|
+
const existing = contributions.get(info.agentId);
|
|
266
|
+
if (existing) {
|
|
267
|
+
existing.commitCount++;
|
|
268
|
+
if (timestamp < existing.firstCommit) {
|
|
269
|
+
existing.firstCommit = timestamp;
|
|
270
|
+
}
|
|
271
|
+
if (timestamp > existing.lastCommit) {
|
|
272
|
+
existing.lastCommit = timestamp;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
contributions.set(info.agentId, {
|
|
277
|
+
agentId: info.agentId,
|
|
278
|
+
agentType: info.agentType ?? 'unknown',
|
|
279
|
+
commitCount: 1,
|
|
280
|
+
firstCommit: timestamp,
|
|
281
|
+
lastCommit: timestamp,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
debug('Failed to get agent contributions:', error);
|
|
288
|
+
}
|
|
289
|
+
return contributions;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get percentage of AI-generated commits in a range
|
|
293
|
+
*/
|
|
294
|
+
export function getAiCommitPercentage(sinceRef, cwd) {
|
|
295
|
+
try {
|
|
296
|
+
const revListArgs = ['rev-list', '--count'];
|
|
297
|
+
revListArgs.push(sinceRef ? `${sinceRef}..HEAD` : 'HEAD');
|
|
298
|
+
const totalCount = parseInt(execFileSync('git', revListArgs, {
|
|
299
|
+
cwd,
|
|
300
|
+
encoding: 'utf-8',
|
|
301
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
302
|
+
}).trim(), 10);
|
|
303
|
+
if (totalCount === 0)
|
|
304
|
+
return 0;
|
|
305
|
+
const commits = getRecentCommitsAgentInfo(totalCount, cwd);
|
|
306
|
+
const aiCount = commits.filter((c) => c.info.isAiGenerated).length;
|
|
307
|
+
return (aiCount / totalCount) * 100;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
debug('Failed to calculate AI commit percentage:', error);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Concurrency Module
|
|
3
|
+
*
|
|
4
|
+
* Provides coordination primitives for multi-agent development scenarios:
|
|
5
|
+
* - Agent identification and registration
|
|
6
|
+
* - File-based distributed locking
|
|
7
|
+
* - Fair request queuing
|
|
8
|
+
* - Git agent attribution
|
|
9
|
+
* - Atomic file operations
|
|
10
|
+
*
|
|
11
|
+
* @module @eddacraft/anvil-runtime/concurrency
|
|
12
|
+
*/
|
|
13
|
+
export * from './types.js';
|
|
14
|
+
export { atomicWriteJson, atomicWriteText, readJsonSafe, readJsonWithRetry, acquireFileLock, tryAcquireFileLock, isLocked, forceReleaseLock, unlinkSafe, fileExists, getFileMtime, sleepWithJitter, type AtomicWriteOptions, type FileLockOptions, type FileLockHandle, } from './atomic.js';
|
|
15
|
+
export { AgentManager, createAgentManager, initializeGlobalAgent, getGlobalAgent, detectAgentType, getAgentId, getSessionId, getAgentName, createAgentInfo, type AgentManagerOptions, } from './agent.js';
|
|
16
|
+
export { LockManager, createLockManager, withLock, tryWithLock, type LockManagerOptions, type AcquireLockOptions, } from './lock-manager.js';
|
|
17
|
+
export { QueueManager, createQueueManager, coordinatedExecution, withConcurrencyLimit, type QueueManagerOptions, type QueueJoinOptions, type QueueWaitOptions, type ConcurrentGroupResult, } from './queue-manager.js';
|
|
18
|
+
export { GIT_TRAILERS, parseCommitTrailers, extractAgentInfo, getCommitAgentInfo, getRecentCommitsAgentInfo, formatCommitWithAgent, prepareCommitMsgHook, getHookSetupCommand, getAgentContributions, getAiCommitPercentage, type CommitAgentInfo, type FormatCommitOptions, type AgentContributionSummary, } from './git-agent.js';
|
|
19
|
+
import { AgentManager } from './agent.js';
|
|
20
|
+
import { LockManager, type LockManagerOptions } from './lock-manager.js';
|
|
21
|
+
import { QueueManager } from './queue-manager.js';
|
|
22
|
+
import type { ConcurrencyConfig, AgentInfo } from './types.js';
|
|
23
|
+
/**
|
|
24
|
+
* Options for creating a full concurrency context
|
|
25
|
+
*/
|
|
26
|
+
export interface ConcurrencyContextOptions {
|
|
27
|
+
/** Workspace root directory */
|
|
28
|
+
workspaceRoot: string;
|
|
29
|
+
/** Concurrency configuration */
|
|
30
|
+
config?: Partial<ConcurrencyConfig>;
|
|
31
|
+
/** Agent info (auto-detected if not provided) */
|
|
32
|
+
agentInfo?: AgentInfo;
|
|
33
|
+
/** Whether to auto-register agent */
|
|
34
|
+
autoRegister?: boolean;
|
|
35
|
+
/** Whether to auto-start heartbeat */
|
|
36
|
+
autoHeartbeat?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Full concurrency context with all managers
|
|
40
|
+
*/
|
|
41
|
+
export interface ConcurrencyContext {
|
|
42
|
+
/** Agent manager */
|
|
43
|
+
agent: AgentManager;
|
|
44
|
+
/** Lock manager */
|
|
45
|
+
locks: LockManager;
|
|
46
|
+
/** Queue manager */
|
|
47
|
+
queue: QueueManager;
|
|
48
|
+
/** Configuration */
|
|
49
|
+
config: ConcurrencyConfig;
|
|
50
|
+
/** Cleanup function */
|
|
51
|
+
cleanup: () => Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a full concurrency context
|
|
55
|
+
*
|
|
56
|
+
* This is the main entry point for multi-agent coordination.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const ctx = await createConcurrencyContext({
|
|
61
|
+
* workspaceRoot: process.cwd(),
|
|
62
|
+
* autoRegister: true,
|
|
63
|
+
* autoHeartbeat: true,
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* try {
|
|
67
|
+
* // Wait for lock with fair queuing
|
|
68
|
+
* const result = await ctx.queue.waitForLock({
|
|
69
|
+
* type: 'action',
|
|
70
|
+
* resource: 'gate',
|
|
71
|
+
* reason: 'Running quality gates',
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* if (result.acquired) {
|
|
75
|
+
* // Perform work
|
|
76
|
+
* await runGates();
|
|
77
|
+
*
|
|
78
|
+
* // Lock is automatically released when leaving queue
|
|
79
|
+
* }
|
|
80
|
+
* } finally {
|
|
81
|
+
* await ctx.cleanup();
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare function createConcurrencyContext(options: ConcurrencyContextOptions): Promise<ConcurrencyContext>;
|
|
86
|
+
/**
|
|
87
|
+
* Create a simple lock-only context (without queuing)
|
|
88
|
+
*
|
|
89
|
+
* Use when you just need basic locking without fair queuing.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createSimpleLockContext(options: LockManagerOptions): {
|
|
92
|
+
locks: LockManager;
|
|
93
|
+
cleanup: () => Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/concurrency/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,eAAe,EACf,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,YAAY,EACZ,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAC;AAMxB,OAAO,EAAE,YAAY,EAAsB,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAqB,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5F,OAAO,EAAE,YAAY,EAAsB,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,+BAA+B;IAC/B,aAAa,EAAE,MAAM,CAAC;IAEtB,gCAAgC;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAEpC,iDAAiD;IACjD,SAAS,CAAC,EAAE,SAAS,CAAC;IAEtB,qCAAqC;IACrC,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,sCAAsC;IACtC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,oBAAoB;IACpB,KAAK,EAAE,YAAY,CAAC;IAEpB,mBAAmB;IACnB,KAAK,EAAE,WAAW,CAAC;IAEnB,oBAAoB;IACpB,KAAK,EAAE,YAAY,CAAC;IAEpB,oBAAoB;IACpB,MAAM,EAAE,iBAAiB,CAAC;IAE1B,uBAAuB;IACvB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,kBAAkB,CAAC,CA8D7B;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG;IACpE,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAUA"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Concurrency Module
|
|
3
|
+
*
|
|
4
|
+
* Provides coordination primitives for multi-agent development scenarios:
|
|
5
|
+
* - Agent identification and registration
|
|
6
|
+
* - File-based distributed locking
|
|
7
|
+
* - Fair request queuing
|
|
8
|
+
* - Git agent attribution
|
|
9
|
+
* - Atomic file operations
|
|
10
|
+
*
|
|
11
|
+
* @module @eddacraft/anvil-runtime/concurrency
|
|
12
|
+
*/
|
|
13
|
+
// Types
|
|
14
|
+
export * from './types.js';
|
|
15
|
+
// Atomic file operations
|
|
16
|
+
export { atomicWriteJson, atomicWriteText, readJsonSafe, readJsonWithRetry, acquireFileLock, tryAcquireFileLock, isLocked, forceReleaseLock, unlinkSafe, fileExists, getFileMtime, sleepWithJitter, } from './atomic.js';
|
|
17
|
+
// Agent management
|
|
18
|
+
export { AgentManager, createAgentManager, initializeGlobalAgent, getGlobalAgent, detectAgentType, getAgentId, getSessionId, getAgentName, createAgentInfo, } from './agent.js';
|
|
19
|
+
// Lock management
|
|
20
|
+
export { LockManager, createLockManager, withLock, tryWithLock, } from './lock-manager.js';
|
|
21
|
+
// Queue management
|
|
22
|
+
export { QueueManager, createQueueManager, coordinatedExecution, withConcurrencyLimit, } from './queue-manager.js';
|
|
23
|
+
// Git agent identification
|
|
24
|
+
export { GIT_TRAILERS, parseCommitTrailers, extractAgentInfo, getCommitAgentInfo, getRecentCommitsAgentInfo, formatCommitWithAgent, prepareCommitMsgHook, getHookSetupCommand, getAgentContributions, getAiCommitPercentage, } from './git-agent.js';
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// High-Level Convenience Functions
|
|
27
|
+
// ============================================================================
|
|
28
|
+
import { createAgentManager } from './agent.js';
|
|
29
|
+
import { createLockManager } from './lock-manager.js';
|
|
30
|
+
import { createQueueManager } from './queue-manager.js';
|
|
31
|
+
import { getDefaultConcurrencyConfig } from './types.js';
|
|
32
|
+
/**
|
|
33
|
+
* Create a full concurrency context
|
|
34
|
+
*
|
|
35
|
+
* This is the main entry point for multi-agent coordination.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const ctx = await createConcurrencyContext({
|
|
40
|
+
* workspaceRoot: process.cwd(),
|
|
41
|
+
* autoRegister: true,
|
|
42
|
+
* autoHeartbeat: true,
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* try {
|
|
46
|
+
* // Wait for lock with fair queuing
|
|
47
|
+
* const result = await ctx.queue.waitForLock({
|
|
48
|
+
* type: 'action',
|
|
49
|
+
* resource: 'gate',
|
|
50
|
+
* reason: 'Running quality gates',
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* if (result.acquired) {
|
|
54
|
+
* // Perform work
|
|
55
|
+
* await runGates();
|
|
56
|
+
*
|
|
57
|
+
* // Lock is automatically released when leaving queue
|
|
58
|
+
* }
|
|
59
|
+
* } finally {
|
|
60
|
+
* await ctx.cleanup();
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export async function createConcurrencyContext(options) {
|
|
65
|
+
const { workspaceRoot, config: configOverrides, agentInfo, autoRegister = true, autoHeartbeat = true, } = options;
|
|
66
|
+
const config = {
|
|
67
|
+
...getDefaultConcurrencyConfig(),
|
|
68
|
+
...configOverrides,
|
|
69
|
+
};
|
|
70
|
+
// Create agent manager
|
|
71
|
+
const agent = createAgentManager({
|
|
72
|
+
workspaceRoot,
|
|
73
|
+
config,
|
|
74
|
+
agentInfo,
|
|
75
|
+
});
|
|
76
|
+
// Create lock manager with same agent
|
|
77
|
+
const locks = createLockManager({
|
|
78
|
+
workspaceRoot,
|
|
79
|
+
config,
|
|
80
|
+
agentInfo: agent.getAgent(),
|
|
81
|
+
});
|
|
82
|
+
// Create queue manager with same lock manager
|
|
83
|
+
const queue = createQueueManager({
|
|
84
|
+
workspaceRoot,
|
|
85
|
+
config,
|
|
86
|
+
agentInfo: agent.getAgent(),
|
|
87
|
+
lockManager: locks,
|
|
88
|
+
});
|
|
89
|
+
// Auto-register and start heartbeat
|
|
90
|
+
if (autoRegister) {
|
|
91
|
+
await agent.register('initializing');
|
|
92
|
+
}
|
|
93
|
+
if (autoHeartbeat) {
|
|
94
|
+
agent.startHeartbeat();
|
|
95
|
+
}
|
|
96
|
+
// Cleanup function
|
|
97
|
+
const cleanup = async () => {
|
|
98
|
+
agent.stopHeartbeat();
|
|
99
|
+
locks.stopAllRenewals();
|
|
100
|
+
await locks.releaseAll();
|
|
101
|
+
if (autoRegister) {
|
|
102
|
+
await agent.unregister();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
agent,
|
|
107
|
+
locks,
|
|
108
|
+
queue,
|
|
109
|
+
config,
|
|
110
|
+
cleanup,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Create a simple lock-only context (without queuing)
|
|
115
|
+
*
|
|
116
|
+
* Use when you just need basic locking without fair queuing.
|
|
117
|
+
*/
|
|
118
|
+
export function createSimpleLockContext(options) {
|
|
119
|
+
const locks = createLockManager(options);
|
|
120
|
+
return {
|
|
121
|
+
locks,
|
|
122
|
+
cleanup: async () => {
|
|
123
|
+
locks.stopAllRenewals();
|
|
124
|
+
await locks.releaseAll();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|