ruv-swarm
Version:
High-performance neural network swarm orchestration in WebAssembly
260 lines (220 loc) • 8.27 kB
JavaScript
/**
* GitHub CLI-based Coordinator for ruv-swarm
* Uses gh CLI for all GitHub operations - simpler and more reliable
*/
const { execSync } = require('child_process');
const fs = require('fs').promises;
const path = require('path');
const Database = require('better-sqlite3');
class GHCoordinator {
constructor(options = {}) {
this.config = {
owner: options.owner || process.env.GITHUB_OWNER,
repo: options.repo || process.env.GITHUB_REPO,
dbPath: options.dbPath || path.join(__dirname, '..', '..', 'data', 'gh-coordinator.db'),
labelPrefix: options.labelPrefix || 'swarm-',
...options,
};
this.db = null;
this.initialize();
}
async initialize() {
// Check if gh CLI is available
try {
execSync('gh --version', { stdio: 'ignore' });
} catch {
throw new Error('GitHub CLI (gh) is not installed. Install it from https://cli.github.com/');
}
// Setup database for local coordination state
await this.setupDatabase();
}
async setupDatabase() {
const dataDir = path.dirname(this.config.dbPath);
await fs.mkdir(dataDir, { recursive: true });
this.db = new Database(this.config.dbPath);
this.db.exec(`
CREATE TABLE IF NOT EXISTS swarm_tasks (
issue_number INTEGER PRIMARY KEY,
swarm_id TEXT,
locked_at INTEGER,
lock_expires INTEGER
);
CREATE TABLE IF NOT EXISTS swarm_registry (
swarm_id TEXT PRIMARY KEY,
user TEXT,
capabilities TEXT,
last_seen INTEGER DEFAULT (strftime('%s', 'now'))
);
`);
}
/**
* Get available tasks from GitHub issues
*/
async getAvailableTasks(filters = {}) {
let cmd = `gh issue list --repo ${this.config.owner}/${this.config.repo} --json number,title,labels,assignees,state,body --limit 100`;
if (filters.label) {
cmd += ` --label "${filters.label}"`;
}
if (filters.state) {
cmd += ` --state ${filters.state}`;
}
const output = execSync(cmd, { encoding: 'utf8' });
const issues = JSON.parse(output);
// Filter out already assigned tasks
const availableIssues = issues.filter(issue => {
// Check if issue has swarm assignment label
const hasSwarmLabel = issue.labels.some(l => l.name.startsWith(this.config.labelPrefix));
// Check if issue is assigned
const isAssigned = issue.assignees.length > 0;
return !hasSwarmLabel && !isAssigned;
});
return availableIssues;
}
/**
* Claim a task for a swarm
*/
async claimTask(swarmId, issueNumber) {
try {
// Add swarm label to issue
const label = `${this.config.labelPrefix}${swarmId}`;
execSync(`gh issue edit ${issueNumber} --repo ${this.config.owner}/${this.config.repo} --add-label "${label}"`, { stdio: 'ignore' });
// Add comment to issue
const comment = `🐝 Task claimed by swarm: ${swarmId}\n\nThis task is being worked on by an automated swarm agent. Updates will be posted as progress is made.`;
execSync(`gh issue comment ${issueNumber} --repo ${this.config.owner}/${this.config.repo} --body "${comment}"`, { stdio: 'ignore' });
// Record in local database
this.db.prepare(`
INSERT OR REPLACE INTO swarm_tasks (issue_number, swarm_id, locked_at, lock_expires)
VALUES (?, ?, strftime('%s', 'now'), strftime('%s', 'now', '+1 hour'))
`).run(issueNumber, swarmId);
return true;
} catch (error) {
console.error(`Failed to claim task ${issueNumber}:`, error.message);
return false;
}
}
/**
* Release a task
*/
async releaseTask(swarmId, issueNumber) {
try {
const label = `${this.config.labelPrefix}${swarmId}`;
execSync(`gh issue edit ${issueNumber} --repo ${this.config.owner}/${this.config.repo} --remove-label "${label}"`, { stdio: 'ignore' });
this.db.prepare('DELETE FROM swarm_tasks WHERE issue_number = ?').run(issueNumber);
return true;
} catch (error) {
console.error(`Failed to release task ${issueNumber}:`, error.message);
return false;
}
}
/**
* Update task progress
*/
async updateTaskProgress(swarmId, issueNumber, message) {
try {
const comment = `🔄 **Progress Update from swarm ${swarmId}**\n\n${message}`;
execSync(`gh issue comment ${issueNumber} --repo ${this.config.owner}/${this.config.repo} --body "${comment}"`, { stdio: 'ignore' });
return true;
} catch (error) {
console.error(`Failed to update task ${issueNumber}:`, error.message);
return false;
}
}
/**
* Create a task allocation PR
*/
async createAllocationPR(allocations) {
const branch = `swarm-allocation-${Date.now()}`;
// Create allocation file
const allocationContent = {
timestamp: new Date().toISOString(),
allocations,
};
const allocationPath = '.github/swarm-allocations.json';
await fs.writeFile(allocationPath, JSON.stringify(allocationContent, null, 2));
// Create PR using gh CLI
try {
execSync(`git checkout -b ${branch}`, { stdio: 'ignore' });
execSync(`git add ${allocationPath}`, { stdio: 'ignore' });
execSync('git commit -m "Update swarm task allocations"', { stdio: 'ignore' });
execSync(`git push origin ${branch}`, { stdio: 'ignore' });
const prBody = `## Swarm Task Allocation Update
This PR updates the task allocation for active swarms.
### Allocations:
${allocations.map(a => `- Issue #${a.issue}: Assigned to swarm ${a.swarm_id}`).join('\n')}
This is an automated update from the swarm coordinator.`;
const output = execSync(`gh pr create --repo ${this.config.owner}/${this.config.repo} --title "Update swarm task allocations" --body "${prBody}" --base main --head ${branch}`, { encoding: 'utf8' });
return output.trim();
} catch (error) {
console.error('Failed to create allocation PR:', error.message);
return null;
}
}
/**
* Get swarm coordination status
*/
async getCoordinationStatus() {
// Get issues with swarm labels
const cmd = `gh issue list --repo ${this.config.owner}/${this.config.repo} --json number,title,labels,assignees --limit 100`;
const output = execSync(cmd, { encoding: 'utf8' });
const issues = JSON.parse(output);
const swarmTasks = issues.filter(issue =>
issue.labels.some(l => l.name.startsWith(this.config.labelPrefix)),
);
// Group by swarm
const swarmStatus = {};
swarmTasks.forEach(issue => {
const swarmLabel = issue.labels.find(l => l.name.startsWith(this.config.labelPrefix));
if (swarmLabel) {
const swarmId = swarmLabel.name.replace(this.config.labelPrefix, '');
if (!swarmStatus[swarmId]) {
swarmStatus[swarmId] = [];
}
swarmStatus[swarmId].push({
number: issue.number,
title: issue.title,
});
}
});
return {
totalIssues: issues.length,
swarmTasks: swarmTasks.length,
availableTasks: issues.length - swarmTasks.length,
swarmStatus,
};
}
/**
* Clean up stale locks
*/
async cleanupStaleLocks() {
const staleTasks = this.db.prepare(`
SELECT issue_number, swarm_id FROM swarm_tasks
WHERE lock_expires < strftime('%s', 'now')
`).all();
for (const task of staleTasks) {
await this.releaseTask(task.swarm_id, task.issue_number);
}
return staleTasks.length;
}
}
// Example usage with gh CLI - commented out to avoid no-unused-vars warning
// async function example() {
// const coordinator = new GHCoordinator({
// owner: 'ruvnet',
// repo: 'ruv-FANN',
// });
//
// // Get available tasks
// const tasks = await coordinator.getAvailableTasks({ state: 'open' });
// console.log(`Found ${tasks.length} available tasks`);
//
// // Claim a task for a swarm
// if (tasks.length > 0) {
// const claimed = await coordinator.claimTask('swarm-123', tasks[0].number);
// console.log(`Claimed task #${tasks[0].number}: ${claimed}`);
// }
//
// // Get coordination status
// const status = await coordinator.getCoordinationStatus();
// console.log('Coordination status:', status);
// }
module.exports = GHCoordinator;