@cluesmith/codev 1.6.0 → 1.6.2
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/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +61 -5
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +29 -12
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/hq-connector.d.ts +23 -0
- package/dist/agent-farm/hq-connector.d.ts.map +1 -0
- package/dist/agent-farm/hq-connector.js +366 -0
- package/dist/agent-farm/hq-connector.js.map +1 -0
- package/dist/agent-farm/servers/dashboard-server.js +46 -0
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +3 -2
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +26 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/init.js.map +1 -1
- package/package.json +5 -2
- package/skeleton/protocols/spider/templates/spec.md +13 -0
- package/templates/dashboard/js/files.js +38 -0
- package/templates/dashboard/js/main.js +3 -0
- package/templates/open.html +9 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HQ Connector - Connects Agent Farm to CODEV_HQ
|
|
3
|
+
*
|
|
4
|
+
* When CODEV_HQ_URL is set, this module:
|
|
5
|
+
* 1. Establishes WebSocket connection to HQ
|
|
6
|
+
* 2. Registers with project info
|
|
7
|
+
* 3. Watches status files and syncs changes
|
|
8
|
+
* 4. Handles approval messages from HQ
|
|
9
|
+
*/
|
|
10
|
+
import { WebSocket } from 'ws';
|
|
11
|
+
import { watch, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join, basename, dirname, relative } from 'node:path';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
import { logger } from './utils/logger.js';
|
|
16
|
+
/**
|
|
17
|
+
* HQ Connector state
|
|
18
|
+
*/
|
|
19
|
+
let ws = null;
|
|
20
|
+
let reconnectTimer = null;
|
|
21
|
+
let pingInterval = null;
|
|
22
|
+
let statusWatcher = null;
|
|
23
|
+
let instanceId = null;
|
|
24
|
+
let projectRoot = null;
|
|
25
|
+
let reconnectAttempts = 0;
|
|
26
|
+
const MAX_RECONNECT_DELAY = 60000;
|
|
27
|
+
const BASE_RECONNECT_DELAY = 1000;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a unique message ID
|
|
30
|
+
*/
|
|
31
|
+
function generateId() {
|
|
32
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Send a message to HQ
|
|
36
|
+
*/
|
|
37
|
+
function sendMessage(message) {
|
|
38
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
39
|
+
logger.debug('[HQ] Cannot send - WebSocket not open');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
ws.send(JSON.stringify(message));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get git remote URL for a project
|
|
46
|
+
*/
|
|
47
|
+
function getGitRemote(projectPath) {
|
|
48
|
+
try {
|
|
49
|
+
const remote = execSync('git remote get-url origin', {
|
|
50
|
+
cwd: projectPath,
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
}).trim();
|
|
54
|
+
return remote || undefined;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get current git SHA for a file
|
|
62
|
+
*/
|
|
63
|
+
function getGitSha(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const sha = execSync(`git log -1 --format=%H -- "${filePath}"`, {
|
|
66
|
+
cwd: dirname(filePath),
|
|
67
|
+
encoding: 'utf-8',
|
|
68
|
+
timeout: 5000,
|
|
69
|
+
}).trim();
|
|
70
|
+
return sha || undefined;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create git commit for approval
|
|
78
|
+
*/
|
|
79
|
+
function commitApproval(filePath, gate, approvedBy) {
|
|
80
|
+
try {
|
|
81
|
+
const dir = dirname(filePath);
|
|
82
|
+
const fileName = basename(filePath);
|
|
83
|
+
execSync(`git add "${fileName}"`, { cwd: dir, timeout: 5000 });
|
|
84
|
+
execSync(`git commit -m "[HQ] Gate approved: ${gate} by ${approvedBy}"`, {
|
|
85
|
+
cwd: dir,
|
|
86
|
+
timeout: 5000,
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
logger.debug(`[HQ] Failed to commit approval: ${error instanceof Error ? error.message : error}`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Handle incoming messages from HQ
|
|
97
|
+
*/
|
|
98
|
+
function handleMessage(data) {
|
|
99
|
+
let message;
|
|
100
|
+
try {
|
|
101
|
+
message = JSON.parse(data);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
logger.debug('[HQ] Invalid message received');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
switch (message.type) {
|
|
108
|
+
case 'welcome':
|
|
109
|
+
logger.info('[HQ] Connected to CODEV_HQ');
|
|
110
|
+
register();
|
|
111
|
+
break;
|
|
112
|
+
case 'pong':
|
|
113
|
+
logger.debug('[HQ] Pong received');
|
|
114
|
+
break;
|
|
115
|
+
case 'response':
|
|
116
|
+
const payload = message.payload;
|
|
117
|
+
if (payload?.success === false) {
|
|
118
|
+
logger.warn(`[HQ] Error response: ${payload.error}`);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case 'approval':
|
|
122
|
+
handleApproval(message);
|
|
123
|
+
break;
|
|
124
|
+
case 'command':
|
|
125
|
+
handleCommand(message);
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
logger.debug(`[HQ] Unknown message type: ${message.type}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Handle approval message from HQ
|
|
133
|
+
*/
|
|
134
|
+
function handleApproval(message) {
|
|
135
|
+
const payload = message.payload;
|
|
136
|
+
logger.info(`[HQ] Received approval for ${payload.gate} from ${payload.approved_by}`);
|
|
137
|
+
// Find the status file for this project
|
|
138
|
+
const statusDir = join(projectRoot, 'codev', 'status');
|
|
139
|
+
const statusFile = join(statusDir, `${payload.project_id}-*.md`);
|
|
140
|
+
// For spike simplicity, just look for any file starting with project_id
|
|
141
|
+
try {
|
|
142
|
+
const { globSync } = require('glob');
|
|
143
|
+
const files = globSync(statusFile);
|
|
144
|
+
if (files.length === 0) {
|
|
145
|
+
logger.warn(`[HQ] Status file not found for project ${payload.project_id}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const filePath = files[0];
|
|
149
|
+
let content = readFileSync(filePath, 'utf-8');
|
|
150
|
+
// Parse YAML frontmatter and update gate status
|
|
151
|
+
// Simple regex-based update for spike
|
|
152
|
+
const gatePattern = new RegExp(`(${payload.gate.replace(/\./g, '\\.')}):\\s*\\{\\s*status:\\s*pending`, 'g');
|
|
153
|
+
const newContent = content.replace(gatePattern, `$1: { status: passed, by: ${payload.approved_by}, at: ${payload.approved_at}`);
|
|
154
|
+
if (newContent !== content) {
|
|
155
|
+
writeFileSync(filePath, newContent, 'utf-8');
|
|
156
|
+
logger.info(`[HQ] Updated ${basename(filePath)} with approval`);
|
|
157
|
+
// Create git commit
|
|
158
|
+
if (commitApproval(filePath, payload.gate, payload.approved_by)) {
|
|
159
|
+
logger.info(`[HQ] Created git commit for approval`);
|
|
160
|
+
}
|
|
161
|
+
// Send status update back to HQ
|
|
162
|
+
sendStatusUpdate(payload.project_path, filePath, newContent);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
logger.error(`[HQ] Failed to handle approval: ${error instanceof Error ? error.message : error}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Handle command from HQ
|
|
171
|
+
*/
|
|
172
|
+
function handleCommand(message) {
|
|
173
|
+
const payload = message.payload;
|
|
174
|
+
logger.info(`[HQ] Received command: ${payload.command}`);
|
|
175
|
+
// For spike, just log commands - full implementation would execute them
|
|
176
|
+
logger.debug(`[HQ] Command args: ${JSON.stringify(payload.args)}`);
|
|
177
|
+
// Acknowledge receipt
|
|
178
|
+
sendMessage({
|
|
179
|
+
type: 'command_ack',
|
|
180
|
+
id: generateId(),
|
|
181
|
+
ts: Date.now(),
|
|
182
|
+
payload: { command_id: message.id, status: 'received' },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Register with HQ
|
|
187
|
+
*/
|
|
188
|
+
function register() {
|
|
189
|
+
if (!projectRoot)
|
|
190
|
+
return;
|
|
191
|
+
const projects = [{
|
|
192
|
+
path: projectRoot,
|
|
193
|
+
name: basename(projectRoot),
|
|
194
|
+
git_remote: getGitRemote(projectRoot),
|
|
195
|
+
}];
|
|
196
|
+
sendMessage({
|
|
197
|
+
type: 'register',
|
|
198
|
+
id: generateId(),
|
|
199
|
+
ts: Date.now(),
|
|
200
|
+
payload: {
|
|
201
|
+
instance_id: instanceId,
|
|
202
|
+
instance_name: process.env.CODEV_HQ_INSTANCE_NAME || `${require('os').hostname()}-agent-farm`,
|
|
203
|
+
version: require('../../package.json').version,
|
|
204
|
+
projects,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
// Start status file watching after registration
|
|
208
|
+
startStatusWatcher();
|
|
209
|
+
// Reset reconnect counter on successful registration
|
|
210
|
+
reconnectAttempts = 0;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Send status file update to HQ
|
|
214
|
+
*/
|
|
215
|
+
function sendStatusUpdate(projectPath, statusFile, content) {
|
|
216
|
+
sendMessage({
|
|
217
|
+
type: 'status_update',
|
|
218
|
+
id: generateId(),
|
|
219
|
+
ts: Date.now(),
|
|
220
|
+
payload: {
|
|
221
|
+
project_path: projectPath,
|
|
222
|
+
status_file: relative(projectPath, statusFile),
|
|
223
|
+
content,
|
|
224
|
+
git_sha: getGitSha(statusFile),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Watch status files for changes
|
|
230
|
+
*/
|
|
231
|
+
function startStatusWatcher() {
|
|
232
|
+
if (statusWatcher)
|
|
233
|
+
return;
|
|
234
|
+
if (!projectRoot)
|
|
235
|
+
return;
|
|
236
|
+
const statusDir = join(projectRoot, 'codev', 'status');
|
|
237
|
+
// Create status directory if it doesn't exist
|
|
238
|
+
if (!existsSync(statusDir)) {
|
|
239
|
+
logger.debug('[HQ] Status directory does not exist, skipping watcher');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
logger.info('[HQ] Watching status files...');
|
|
243
|
+
statusWatcher = watch(statusDir, (eventType, filename) => {
|
|
244
|
+
if (!filename || !filename.endsWith('.md'))
|
|
245
|
+
return;
|
|
246
|
+
const filePath = join(statusDir, filename);
|
|
247
|
+
if (!existsSync(filePath))
|
|
248
|
+
return;
|
|
249
|
+
try {
|
|
250
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
251
|
+
sendStatusUpdate(projectRoot, filePath, content);
|
|
252
|
+
logger.debug(`[HQ] Synced ${filename}`);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
logger.debug(`[HQ] Failed to read ${filename}: ${error instanceof Error ? error.message : error}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Start ping interval to keep connection alive
|
|
261
|
+
*/
|
|
262
|
+
function startPingInterval() {
|
|
263
|
+
if (pingInterval)
|
|
264
|
+
return;
|
|
265
|
+
pingInterval = setInterval(() => {
|
|
266
|
+
sendMessage({
|
|
267
|
+
type: 'ping',
|
|
268
|
+
id: generateId(),
|
|
269
|
+
ts: Date.now(),
|
|
270
|
+
payload: { ts: Date.now() },
|
|
271
|
+
});
|
|
272
|
+
}, 30000);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Attempt to reconnect to HQ
|
|
276
|
+
*/
|
|
277
|
+
function scheduleReconnect() {
|
|
278
|
+
if (reconnectTimer)
|
|
279
|
+
return;
|
|
280
|
+
const delay = Math.min(BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
|
|
281
|
+
reconnectAttempts++;
|
|
282
|
+
logger.info(`[HQ] Reconnecting in ${delay / 1000}s...`);
|
|
283
|
+
reconnectTimer = setTimeout(() => {
|
|
284
|
+
reconnectTimer = null;
|
|
285
|
+
connect();
|
|
286
|
+
}, delay);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Connect to CODEV_HQ
|
|
290
|
+
*/
|
|
291
|
+
function connect() {
|
|
292
|
+
const hqUrl = process.env.CODEV_HQ_URL;
|
|
293
|
+
if (!hqUrl)
|
|
294
|
+
return;
|
|
295
|
+
logger.info(`[HQ] Connecting to ${hqUrl}...`);
|
|
296
|
+
const apiKey = process.env.CODEV_HQ_API_KEY || 'dev-key-spike';
|
|
297
|
+
const wsUrl = new URL(hqUrl);
|
|
298
|
+
wsUrl.searchParams.set('key', apiKey);
|
|
299
|
+
ws = new WebSocket(wsUrl.toString());
|
|
300
|
+
ws.on('open', () => {
|
|
301
|
+
logger.info('[HQ] WebSocket connected');
|
|
302
|
+
startPingInterval();
|
|
303
|
+
});
|
|
304
|
+
ws.on('message', (data) => {
|
|
305
|
+
handleMessage(data.toString());
|
|
306
|
+
});
|
|
307
|
+
ws.on('close', (code, reason) => {
|
|
308
|
+
logger.warn(`[HQ] Disconnected (code: ${code})`);
|
|
309
|
+
cleanup();
|
|
310
|
+
scheduleReconnect();
|
|
311
|
+
});
|
|
312
|
+
ws.on('error', (error) => {
|
|
313
|
+
logger.error(`[HQ] WebSocket error: ${error.message}`);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Cleanup resources
|
|
318
|
+
*/
|
|
319
|
+
function cleanup() {
|
|
320
|
+
if (pingInterval) {
|
|
321
|
+
clearInterval(pingInterval);
|
|
322
|
+
pingInterval = null;
|
|
323
|
+
}
|
|
324
|
+
if (statusWatcher) {
|
|
325
|
+
statusWatcher.close();
|
|
326
|
+
statusWatcher = null;
|
|
327
|
+
}
|
|
328
|
+
ws = null;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Initialize HQ connector
|
|
332
|
+
* Called from af start when CODEV_HQ_URL is set
|
|
333
|
+
*/
|
|
334
|
+
export function initHQConnector(root) {
|
|
335
|
+
const hqUrl = process.env.CODEV_HQ_URL;
|
|
336
|
+
if (!hqUrl) {
|
|
337
|
+
logger.debug('[HQ] CODEV_HQ_URL not set, HQ connector disabled');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
instanceId = randomUUID();
|
|
341
|
+
projectRoot = root;
|
|
342
|
+
logger.info('[HQ] HQ connector enabled');
|
|
343
|
+
connect();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Stop HQ connector
|
|
347
|
+
*/
|
|
348
|
+
export function stopHQConnector() {
|
|
349
|
+
if (reconnectTimer) {
|
|
350
|
+
clearTimeout(reconnectTimer);
|
|
351
|
+
reconnectTimer = null;
|
|
352
|
+
}
|
|
353
|
+
cleanup();
|
|
354
|
+
if (ws) {
|
|
355
|
+
ws.close();
|
|
356
|
+
ws = null;
|
|
357
|
+
}
|
|
358
|
+
logger.info('[HQ] Disconnected from CODEV_HQ');
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Check if HQ connector is enabled
|
|
362
|
+
*/
|
|
363
|
+
export function isHQEnabled() {
|
|
364
|
+
return !!process.env.CODEV_HQ_URL;
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=hq-connector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hq-connector.js","sourceRoot":"","sources":["../../src/agent-farm/hq-connector.ts"],"names":[],"mappings":"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"}
|
|
@@ -1552,6 +1552,52 @@ const server = http.createServer(async (req, res) => {
|
|
|
1552
1552
|
res.end(JSON.stringify(tree));
|
|
1553
1553
|
return;
|
|
1554
1554
|
}
|
|
1555
|
+
// API: Get hash of file tree for change detection (auto-refresh)
|
|
1556
|
+
if (req.method === 'GET' && url.pathname === '/api/files/hash') {
|
|
1557
|
+
// Build a lightweight hash based on directory mtimes
|
|
1558
|
+
// This is faster than building the full tree
|
|
1559
|
+
function getTreeHash(dirPath) {
|
|
1560
|
+
const EXCLUDED_DIRS = new Set([
|
|
1561
|
+
'node_modules', '.git', 'dist', '__pycache__', '.next',
|
|
1562
|
+
'.nuxt', '.turbo', 'coverage', '.nyc_output', '.cache',
|
|
1563
|
+
'.parcel-cache', 'build', '.svelte-kit', 'vendor', '.venv', 'venv', 'env',
|
|
1564
|
+
]);
|
|
1565
|
+
let hash = '';
|
|
1566
|
+
function walk(dir) {
|
|
1567
|
+
try {
|
|
1568
|
+
const stat = fs.statSync(dir);
|
|
1569
|
+
hash += `${dir}:${stat.mtimeMs};`;
|
|
1570
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
1571
|
+
for (const item of items) {
|
|
1572
|
+
if (EXCLUDED_DIRS.has(item.name))
|
|
1573
|
+
continue;
|
|
1574
|
+
if (item.isDirectory()) {
|
|
1575
|
+
walk(path.join(dir, item.name));
|
|
1576
|
+
}
|
|
1577
|
+
else if (item.isFile()) {
|
|
1578
|
+
// Include file mtime for change detection
|
|
1579
|
+
const fileStat = fs.statSync(path.join(dir, item.name));
|
|
1580
|
+
hash += `${item.name}:${fileStat.mtimeMs};`;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
catch {
|
|
1585
|
+
// Ignore errors
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
walk(dirPath);
|
|
1589
|
+
// Simple hash: sum of char codes
|
|
1590
|
+
let sum = 0;
|
|
1591
|
+
for (let i = 0; i < hash.length; i++) {
|
|
1592
|
+
sum = ((sum << 5) - sum + hash.charCodeAt(i)) | 0;
|
|
1593
|
+
}
|
|
1594
|
+
return sum.toString(16);
|
|
1595
|
+
}
|
|
1596
|
+
const hash = getTreeHash(projectRoot);
|
|
1597
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1598
|
+
res.end(JSON.stringify({ hash }));
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1555
1601
|
// API: Create a new file (Bugfix #131)
|
|
1556
1602
|
if (req.method === 'POST' && url.pathname === '/api/files') {
|
|
1557
1603
|
const body = await parseJsonBody(req);
|