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
JavaScript
/**
* 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