UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

863 lines 35.8 kB
/** * Claims MCP Tools for CLI * * Implements MCP tools for ADR-016: Collaborative Issue Claims * Provides programmatic access to claim operations for MCP clients. * * @module @claude-flow/cli/mcp-tools/claims */ import { validateIdentifier, validateText } from './validate-input.js'; // File-based persistence import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join, resolve } from 'path'; const CLAIMS_DIR = '.claude-flow/claims'; const CLAIMS_FILE = 'claims.json'; function getClaimsPath() { return resolve(join(CLAIMS_DIR, CLAIMS_FILE)); } function ensureClaimsDir() { const dir = resolve(CLAIMS_DIR); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } function loadClaims() { try { const path = getClaimsPath(); if (existsSync(path)) { return JSON.parse(readFileSync(path, 'utf-8')); } } catch { // Return empty store on error } return { claims: {}, stealable: {}, contests: {} }; } function saveClaims(store) { ensureClaimsDir(); writeFileSync(getClaimsPath(), JSON.stringify(store, null, 2), 'utf-8'); } function formatClaimant(claimant) { return claimant.type === 'human' ? `human:${claimant.userId}:${claimant.name}` : `agent:${claimant.agentId}:${claimant.agentType}`; } function parseClaimant(str) { const parts = str.split(':'); if (parts[0] === 'human' && parts.length >= 3) { return { type: 'human', userId: parts[1], name: parts.slice(2).join(':') }; } else if (parts[0] === 'agent' && parts.length >= 3) { return { type: 'agent', agentId: parts[1], agentType: parts[2] }; } return null; } export const claimsTools = [ { name: 'claims_claim', description: 'Claim an issue for work (human or agent) Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { issueId: { type: 'string', description: 'Issue ID or GitHub issue number', }, claimant: { type: 'string', description: 'Claimant identifier (e.g., "human:user-1:Alice" or "agent:coder-1:coder")', }, context: { type: 'string', description: 'Optional context about the work approach', }, }, required: ['issueId', 'claimant'], }, handler: async (input) => { const issueId = input.issueId; const claimantStr = input.claimant; const context = input.context; { const v = validateIdentifier(issueId, 'issueId'); if (!v.valid) return { success: false, error: v.error }; } { const v = validateText(claimantStr, 'claimant'); if (!v.valid) return { success: false, error: v.error }; } if (context) { const v = validateText(context, 'context'); if (!v.valid) return { success: false, error: v.error }; } const claimant = parseClaimant(claimantStr); if (!claimant) { return { success: false, error: 'Invalid claimant format. Use "human:userId:name" or "agent:agentId:agentType"' }; } const store = loadClaims(); // Check if already claimed if (store.claims[issueId]) { const existing = store.claims[issueId]; return { success: false, error: `Issue already claimed by ${formatClaimant(existing.claimant)}`, existingClaim: existing, }; } const now = new Date().toISOString(); const claim = { issueId, claimant, claimedAt: now, status: 'active', statusChangedAt: now, progress: 0, context, }; store.claims[issueId] = claim; saveClaims(store); return { success: true, claim, message: `Issue ${issueId} claimed by ${formatClaimant(claimant)}`, }; }, }, { name: 'claims_release', description: 'Release a claim on an issue Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { issueId: { type: 'string', description: 'Issue ID to release', }, claimant: { type: 'string', description: 'Claimant identifier (must match current owner)', }, reason: { type: 'string', description: 'Reason for releasing', }, }, required: ['issueId', 'claimant'], }, handler: async (input) => { const issueId = input.issueId; const claimantStr = input.claimant; const reason = input.reason; { const v = validateIdentifier(issueId, 'issueId'); if (!v.valid) return { success: false, error: v.error }; } { const v = validateText(claimantStr, 'claimant'); if (!v.valid) return { success: false, error: v.error }; } if (reason) { const v = validateText(reason, 'reason'); if (!v.valid) return { success: false, error: v.error }; } const claimant = parseClaimant(claimantStr); if (!claimant) { return { success: false, error: 'Invalid claimant format' }; } const store = loadClaims(); const claim = store.claims[issueId]; if (!claim) { return { success: false, error: 'Issue is not claimed' }; } // Verify ownership if (formatClaimant(claim.claimant) !== formatClaimant(claimant)) { return { success: false, error: 'Only the current claimant can release' }; } delete store.claims[issueId]; delete store.stealable[issueId]; saveClaims(store); return { success: true, message: `Issue ${issueId} released`, reason, previousClaim: claim, }; }, }, { name: 'claims_handoff', description: 'Request handoff of an issue to another claimant Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { issueId: { type: 'string', description: 'Issue ID to handoff', }, from: { type: 'string', description: 'Current claimant identifier', }, to: { type: 'string', description: 'Target claimant identifier', }, reason: { type: 'string', description: 'Reason for handoff', }, progress: { type: 'number', description: 'Current progress percentage (0-100)', }, }, required: ['issueId', 'from', 'to'], }, handler: async (input) => { const issueId = input.issueId; const fromStr = input.from; const toStr = input.to; const reason = input.reason; const progress = input.progress || 0; { const v = validateIdentifier(issueId, 'issueId'); if (!v.valid) return { success: false, error: v.error }; } { const v = validateText(fromStr, 'from'); if (!v.valid) return { success: false, error: v.error }; } { const v = validateText(toStr, 'to'); if (!v.valid) return { success: false, error: v.error }; } if (reason) { const v = validateText(reason, 'reason'); if (!v.valid) return { success: false, error: v.error }; } const from = parseClaimant(fromStr); const to = parseClaimant(toStr); if (!from || !to) { return { success: false, error: 'Invalid claimant format' }; } const store = loadClaims(); const claim = store.claims[issueId]; if (!claim) { return { success: false, error: 'Issue is not claimed' }; } if (formatClaimant(claim.claimant) !== formatClaimant(from)) { return { success: false, error: 'Only the current claimant can request handoff' }; } const now = new Date().toISOString(); claim.status = 'handoff-pending'; claim.statusChangedAt = now; claim.handoffTo = to; claim.handoffReason = reason; claim.progress = progress; store.claims[issueId] = claim; saveClaims(store); return { success: true, claim, message: `Handoff requested from ${formatClaimant(from)} to ${formatClaimant(to)}`, }; }, }, { name: 'claims_accept-handoff', description: 'Accept a pending handoff Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { issueId: { type: 'string', description: 'Issue ID with pending handoff', }, claimant: { type: 'string', description: 'Claimant accepting the handoff', }, }, required: ['issueId', 'claimant'], }, handler: async (input) => { const issueId = input.issueId; const claimantStr = input.claimant; { const v = validateIdentifier(issueId, 'issueId'); if (!v.valid) return { success: false, error: v.error }; } { const v = validateText(claimantStr, 'claimant'); if (!v.valid) return { success: false, error: v.error }; } const claimant = parseClaimant(claimantStr); if (!claimant) { return { success: false, error: 'Invalid claimant format' }; } const store = loadClaims(); const claim = store.claims[issueId]; if (!claim) { return { success: false, error: 'Issue is not claimed' }; } if (claim.status !== 'handoff-pending') { return { success: false, error: 'No pending handoff for this issue' }; } if (!claim.handoffTo || formatClaimant(claim.handoffTo) !== formatClaimant(claimant)) { return { success: false, error: 'You are not the target of this handoff' }; } const previousOwner = claim.claimant; const now = new Date().toISOString(); claim.claimant = claimant; claim.status = 'active'; claim.statusChangedAt = now; claim.handoffTo = undefined; claim.handoffReason = undefined; store.claims[issueId] = claim; saveClaims(store); return { success: true, claim, previousOwner, message: `Handoff accepted. ${formatClaimant(claimant)} now owns issue ${issueId}`, }; }, }, { name: 'claims_status', description: 'Update claim status Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { issueId: { type: 'string', description: 'Issue ID', }, status: { type: 'string', description: 'New status', enum: ['active', 'paused', 'blocked', 'review-requested', 'completed'], }, note: { type: 'string', description: 'Status note or reason', }, progress: { type: 'number', description: 'Current progress percentage', }, }, required: ['issueId', 'status'], }, handler: async (input) => { const issueId = input.issueId; const status = input.status; const note = input.note; const progress = input.progress; { const v = validateIdentifier(issueId, 'issueId'); if (!v.valid) return { success: false, error: v.error }; } if (note) { const v = validateText(note, 'note'); if (!v.valid) return { success: false, error: v.error }; } const store = loadClaims(); const claim = store.claims[issueId]; if (!claim) { return { success: false, error: 'Issue is not claimed' }; } const now = new Date().toISOString(); claim.status = status; claim.statusChangedAt = now; if (status === 'blocked') { claim.blockReason = note; } if (progress !== undefined) { claim.progress = Math.min(100, Math.max(0, progress)); } store.claims[issueId] = claim; saveClaims(store); return { success: true, claim, message: `Issue ${issueId} status updated to ${status}`, }; }, }, { name: 'claims_list', description: 'List all claims or filter by criteria Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { status: { type: 'string', description: 'Filter by status', enum: ['active', 'paused', 'blocked', 'stealable', 'completed', 'all'], }, claimant: { type: 'string', description: 'Filter by claimant', }, agentType: { type: 'string', description: 'Filter by agent type', }, }, }, handler: async (input) => { const status = input.status; const claimantFilter = input.claimant; const agentType = input.agentType; if (claimantFilter) { const v = validateText(claimantFilter, 'claimant'); if (!v.valid) return { success: false, error: v.error }; } if (agentType) { const v = validateIdentifier(agentType, 'agentType'); if (!v.valid) return { success: false, error: v.error }; } const store = loadClaims(); let claims = Object.values(store.claims); if (status && status !== 'all') { claims = claims.filter(c => c.status === status); } if (claimantFilter) { claims = claims.filter(c => formatClaimant(c.claimant).includes(claimantFilter)); } if (agentType) { claims = claims.filter(c => c.claimant.type === 'agent' && c.claimant.agentType === agentType); } return { success: true, claims, count: claims.length, stealableCount: Object.keys(store.stealable).length, }; }, }, { name: 'claims_mark-stealable', description: 'Mark an issue as stealable by other agents Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { issueId: { type: 'string', description: 'Issue ID to mark stealable', }, reason: { type: 'string', description: 'Reason for marking stealable', enum: ['overloaded', 'stale', 'blocked-timeout', 'voluntary'], }, preferredTypes: { type: 'array', description: 'Preferred agent types to steal', items: { type: 'string' }, }, context: { type: 'string', description: 'Handoff context for the stealer', }, }, required: ['issueId', 'reason'], }, handler: async (input) => { const issueId = input.issueId; const reason = input.reason; const preferredTypes = input.preferredTypes; const context = input.context; { const v = validateIdentifier(issueId, 'issueId'); if (!v.valid) return { success: false, error: v.error }; } if (context) { const v = validateText(context, 'context'); if (!v.valid) return { success: false, error: v.error }; } const store = loadClaims(); const claim = store.claims[issueId]; if (!claim) { return { success: false, error: 'Issue is not claimed' }; } const now = new Date().toISOString(); claim.status = 'stealable'; claim.statusChangedAt = now; store.stealable[issueId] = { reason, stealableAt: now, preferredTypes, progress: claim.progress, context, }; store.claims[issueId] = claim; saveClaims(store); return { success: true, claim, stealableInfo: store.stealable[issueId], message: `Issue ${issueId} marked as stealable (${reason})`, }; }, }, { name: 'claims_steal', description: 'Steal a stealable issue Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { issueId: { type: 'string', description: 'Issue ID to steal', }, stealer: { type: 'string', description: 'Claimant stealing the issue', }, }, required: ['issueId', 'stealer'], }, handler: async (input) => { const issueId = input.issueId; const stealerStr = input.stealer; { const v = validateIdentifier(issueId, 'issueId'); if (!v.valid) return { success: false, error: v.error }; } { const v = validateText(stealerStr, 'stealer'); if (!v.valid) return { success: false, error: v.error }; } const stealer = parseClaimant(stealerStr); if (!stealer) { return { success: false, error: 'Invalid claimant format' }; } const store = loadClaims(); const claim = store.claims[issueId]; const stealableInfo = store.stealable[issueId]; if (!claim) { return { success: false, error: 'Issue is not claimed' }; } if (!stealableInfo) { return { success: false, error: 'Issue is not stealable' }; } // Check preferred types if (stealableInfo.preferredTypes && stealableInfo.preferredTypes.length > 0) { if (stealer.type === 'agent' && !stealableInfo.preferredTypes.includes(stealer.agentType)) { return { success: false, error: `Issue prefers agent types: ${stealableInfo.preferredTypes.join(', ')}`, }; } } const previousOwner = claim.claimant; const now = new Date().toISOString(); claim.claimant = stealer; claim.status = 'active'; claim.statusChangedAt = now; claim.context = stealableInfo.context; delete store.stealable[issueId]; store.claims[issueId] = claim; saveClaims(store); return { success: true, claim, previousOwner, stealableInfo, message: `Issue ${issueId} stolen by ${formatClaimant(stealer)}`, }; }, }, { name: 'claims_stealable', description: 'List all stealable issues Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { agentType: { type: 'string', description: 'Filter by preferred agent type', }, }, }, handler: async (input) => { const agentType = input.agentType; if (agentType) { const v = validateIdentifier(agentType, 'agentType'); if (!v.valid) return { success: false, error: v.error }; } const store = loadClaims(); let stealableIssues = Object.entries(store.stealable).map(([issueId, info]) => ({ issueId, ...info, claim: store.claims[issueId], })); if (agentType) { stealableIssues = stealableIssues.filter(s => !s.preferredTypes || s.preferredTypes.length === 0 || s.preferredTypes.includes(agentType)); } return { success: true, stealable: stealableIssues, count: stealableIssues.length, }; }, }, { name: 'claims_load', description: 'Get agent load information Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { agentId: { type: 'string', description: 'Specific agent ID (optional)', }, agentType: { type: 'string', description: 'Filter by agent type', }, }, }, handler: async (input) => { const agentId = input.agentId; const agentType = input.agentType; if (agentId) { const v = validateIdentifier(agentId, 'agentId'); if (!v.valid) return { success: false, error: v.error }; } if (agentType) { const v = validateIdentifier(agentType, 'agentType'); if (!v.valid) return { success: false, error: v.error }; } const store = loadClaims(); const claims = Object.values(store.claims); // Group claims by agent const agentLoads = new Map(); for (const claim of claims) { if (claim.claimant.type !== 'agent') continue; const key = claim.claimant.agentId; if (!agentLoads.has(key)) { agentLoads.set(key, { agentId: key, agentType: claim.claimant.agentType, claims: [], blockedCount: 0, }); } const load = agentLoads.get(key); load.claims.push(claim); if (claim.status === 'blocked') { load.blockedCount++; } } let loads = Array.from(agentLoads.values()); if (agentId) { loads = loads.filter(l => l.agentId === agentId); } if (agentType) { loads = loads.filter(l => l.agentType === agentType); } const result = loads.map(l => ({ agentId: l.agentId, agentType: l.agentType, claimCount: l.claims.length, maxClaims: 5, // Default max utilization: l.claims.length / 5, blockedCount: l.blockedCount, claims: l.claims.map(c => ({ issueId: c.issueId, status: c.status, progress: c.progress, })), })); return { success: true, loads: result, totalAgents: result.length, totalClaims: claims.filter(c => c.claimant.type === 'agent').length, avgUtilization: result.length > 0 ? result.reduce((sum, l) => sum + l.utilization, 0) / result.length : 0, }; }, }, { name: 'claims_board', description: 'Get a visual board view of all claims Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: {}, }, handler: async () => { const store = loadClaims(); const claims = Object.values(store.claims); const byStatus = { active: [], paused: [], blocked: [], 'handoff-pending': [], 'review-requested': [], stealable: [], completed: [], }; for (const claim of claims) { if (byStatus[claim.status]) { byStatus[claim.status].push(claim); } } const humanClaims = claims.filter(c => c.claimant.type === 'human'); const agentClaims = claims.filter(c => c.claimant.type === 'agent'); return { success: true, board: { active: byStatus.active.map(c => ({ issueId: c.issueId, claimant: formatClaimant(c.claimant), progress: c.progress })), paused: byStatus.paused.map(c => ({ issueId: c.issueId, claimant: formatClaimant(c.claimant) })), blocked: byStatus.blocked.map(c => ({ issueId: c.issueId, claimant: formatClaimant(c.claimant), reason: c.blockReason })), 'handoff-pending': byStatus['handoff-pending'].map(c => ({ issueId: c.issueId, from: formatClaimant(c.claimant), to: c.handoffTo ? formatClaimant(c.handoffTo) : null })), 'review-requested': byStatus['review-requested'].map(c => ({ issueId: c.issueId, claimant: formatClaimant(c.claimant) })), stealable: byStatus.stealable.map(c => ({ issueId: c.issueId, claimant: formatClaimant(c.claimant) })), completed: byStatus.completed.map(c => ({ issueId: c.issueId, claimant: formatClaimant(c.claimant) })), }, summary: { total: claims.length, active: byStatus.active.length, blocked: byStatus.blocked.length, stealable: byStatus.stealable.length, humanClaims: humanClaims.length, agentClaims: agentClaims.length, }, }; }, }, { name: 'claims_rebalance', description: 'Suggest or apply load rebalancing across agents Use when nothing native covers per-agent capability gating — Claude Code agents have file-system access by default. Pair claims_grant + claims_check before letting an agent run privileged ops. For trusted in-session work, no claims call is needed.', category: 'claims', inputSchema: { type: 'object', properties: { dryRun: { type: 'boolean', description: 'Preview rebalancing without applying', default: true, }, targetUtilization: { type: 'number', description: 'Target utilization (0-1)', default: 0.7, }, }, }, handler: async (input) => { const dryRun = input.dryRun !== false; const targetUtilization = input.targetUtilization || 0.7; const store = loadClaims(); const claims = Object.values(store.claims); // Group by agent const agentLoads = new Map(); for (const claim of claims) { if (claim.claimant.type !== 'agent') continue; const key = claim.claimant.agentId; if (!agentLoads.has(key)) { agentLoads.set(key, { agentId: key, agentType: claim.claimant.agentType, claims: [] }); } agentLoads.get(key).claims.push(claim); } const loads = Array.from(agentLoads.values()); const maxClaims = 5; const avgLoad = loads.length > 0 ? loads.reduce((sum, l) => sum + l.claims.length, 0) / loads.length : 0; const overloaded = loads.filter(l => l.claims.length > maxClaims * targetUtilization * 1.5); const underloaded = loads.filter(l => l.claims.length < maxClaims * targetUtilization * 0.5); const suggestions = []; for (const over of overloaded) { // Find low-progress claims to redistribute const movable = over.claims .filter(c => c.progress < 25 && c.status === 'active') .slice(0, over.claims.length - Math.ceil(maxClaims * targetUtilization)); for (const claim of movable) { const target = underloaded.find(u => u.agentType === over.agentType && u.claims.length < maxClaims); if (target) { suggestions.push({ issueId: claim.issueId, from: `agent:${over.agentId}:${over.agentType}`, to: `agent:${target.agentId}:${target.agentType}`, reason: 'Load balancing', }); } } } // When not a dry run, execute the suggested moves if (!dryRun) { for (const suggestion of suggestions) { const claim = store.claims[suggestion.issueId]; if (claim) { const newOwner = parseClaimant(suggestion.to); if (newOwner) { claim.claimant = newOwner; claim.statusChangedAt = new Date().toISOString(); store.claims[suggestion.issueId] = claim; } } } saveClaims(store); } return { success: true, dryRun, suggestions, metrics: { totalAgents: loads.length, avgLoad, overloadedCount: overloaded.length, underloadedCount: underloaded.length, targetUtilization, }, message: dryRun ? `Found ${suggestions.length} rebalancing opportunities (dry run)` : `Applied ${suggestions.length} rebalancing moves`, }; }, }, ]; export default claimsTools; //# sourceMappingURL=claims-tools.js.map