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
1,460 lines (1,281 loc) • 43.9 kB
text/typescript
// @ts-nocheck - CLI integration requires the full @claude-flow/cli package
/**
* V3 CLI Claims Command
* Issue claiming and work distribution management
*
* Implements:
* - Core claiming commands (list, claim, release, handoff, status)
* - Work stealing commands (stealable, steal, mark-stealable, contest)
* - Load balancing commands (load, rebalance)
*/
import type { Command, CommandContext, CommandResult } from './cli-types.js';
import { output, select, confirm, input, callMCPTool, MCPClientError } from './cli-types.js';
// ============================================
// Types
// ============================================
export interface ClaimServices {
claimIssue: (issueId: string, claimantId: string, claimantType: ClaimantType) => Promise<Claim>;
releaseClaim: (issueId: string, claimantId: string) => Promise<void>;
requestHandoff: (issueId: string, targetId: string, targetType: ClaimantType) => Promise<HandoffRequest>;
updateStatus: (issueId: string, status: ClaimStatus, reason?: string) => Promise<Claim>;
listClaims: (filter?: ClaimFilter) => Promise<Claim[]>;
listStealable: () => Promise<Claim[]>;
stealIssue: (issueId: string, stealerId: string) => Promise<Claim>;
markStealable: (issueId: string, reason?: string) => Promise<Claim>;
contestSteal: (issueId: string, contesterId: string, reason: string) => Promise<ContestResult>;
getAgentLoad: (agentId?: string) => Promise<AgentLoad[]>;
rebalance: (dryRun?: boolean) => Promise<RebalanceResult>;
}
export type ClaimantType = 'agent' | 'human';
export type ClaimStatus = 'active' | 'blocked' | 'review-requested' | 'stealable' | 'completed';
export interface Claim {
issueId: string;
claimantId: string;
claimantType: ClaimantType;
status: ClaimStatus;
progress: number;
claimedAt: string;
expiresAt?: string;
blockedReason?: string;
stealableReason?: string;
}
export interface ClaimFilter {
claimantId?: string;
status?: ClaimStatus;
available?: boolean;
}
export interface HandoffRequest {
issueId: string;
fromId: string;
toId: string;
toType: ClaimantType;
requestedAt: string;
status: 'pending' | 'accepted' | 'rejected';
}
export interface ContestResult {
issueId: string;
contesterId: string;
originalClaimantId: string;
resolution: 'steal-reverted' | 'steal-upheld' | 'pending-review';
reason?: string;
}
export interface AgentLoad {
agentId: string;
agentType: string;
activeIssues: number;
totalCapacity: number;
utilizationPercent: number;
avgCompletionTime: string;
status: 'healthy' | 'overloaded' | 'idle';
}
export interface RebalanceResult {
moved: number;
reassignments: Array<{
issueId: string;
fromAgent: string;
toAgent: string;
reason: string;
}>;
skipped: number;
dryRun: boolean;
}
// ============================================
// Formatting Helpers
// ============================================
function formatClaimStatus(status: ClaimStatus): string {
switch (status) {
case 'active':
return output.success(status);
case 'blocked':
return output.error(status);
case 'review-requested':
return output.warning(status);
case 'stealable':
return output.warning(status);
case 'completed':
return output.dim(status);
default:
return status;
}
}
function formatClaimantType(type: ClaimantType): string {
switch (type) {
case 'agent':
return output.info(type);
case 'human':
return output.highlight(type);
default:
return type;
}
}
function formatAgentStatus(status: 'healthy' | 'overloaded' | 'idle'): string {
switch (status) {
case 'healthy':
return output.success(status);
case 'overloaded':
return output.error(status);
case 'idle':
return output.dim(status);
default:
return status;
}
}
function formatProgress(progress: number): string {
if (progress >= 75) {
return output.success(`${progress}%`);
} else if (progress >= 25) {
return output.warning(`${progress}%`);
}
return output.dim(`${progress}%`);
}
function formatTimeRemaining(expiresAt?: string): string {
if (!expiresAt) return output.dim('N/A');
const expiry = new Date(expiresAt);
const now = new Date();
const diffMs = expiry.getTime() - now.getTime();
if (diffMs <= 0) {
return output.error('EXPIRED');
}
const hours = Math.floor(diffMs / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
if (hours < 1) {
return output.warning(`${minutes}m`);
} else if (hours < 4) {
return output.warning(`${hours}h ${minutes}m`);
}
return output.dim(`${hours}h ${minutes}m`);
}
function parseTarget(target: string): { id: string; type: ClaimantType } {
// Format: agent:coder-1 or human:alice
const [type, id] = target.split(':');
if (!type || !id || (type !== 'agent' && type !== 'human')) {
throw new Error(`Invalid target format: ${target}. Use agent:<id> or human:<id>`);
}
return { id, type: type as ClaimantType };
}
// ============================================
// List Subcommand
// ============================================
const listCommand: Command = {
name: 'list',
aliases: ['ls'],
description: 'List issues',
options: [
{
name: 'available',
short: 'a',
description: 'Show only unclaimed issues',
type: 'boolean',
default: false
},
{
name: 'mine',
short: 'm',
description: 'Show only my claims',
type: 'boolean',
default: false
},
{
name: 'status',
short: 's',
description: 'Filter by status',
type: 'string',
choices: ['active', 'blocked', 'review-requested', 'stealable', 'completed']
},
{
name: 'limit',
short: 'l',
description: 'Maximum number of issues to show',
type: 'number',
default: 20
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const available = ctx.flags.available as boolean;
const mine = ctx.flags.mine as boolean;
const status = ctx.flags.status as ClaimStatus | undefined;
const limit = ctx.flags.limit as number;
try {
const result = await callMCPTool<{
claims: Claim[];
total: number;
available: number;
}>('claims/list', {
available,
mine,
status,
limit
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
if (available) {
output.writeln(output.bold('Available Issues (Unclaimed)'));
} else if (mine) {
output.writeln(output.bold('My Claims'));
} else {
output.writeln(output.bold('All Claims'));
}
output.writeln();
if (result.claims.length === 0) {
if (available) {
output.printInfo('No unclaimed issues available');
} else if (mine) {
output.printInfo('You have no active claims');
} else {
output.printInfo('No claims found matching criteria');
}
return { success: true, data: result };
}
output.printTable({
columns: [
{ key: 'issueId', header: 'Issue', width: 12 },
{ key: 'claimant', header: 'Claimant', width: 15 },
{ key: 'type', header: 'Type', width: 8 },
{ key: 'status', header: 'Status', width: 16 },
{ key: 'progress', header: 'Progress', width: 10 },
{ key: 'time', header: 'Time Left', width: 12 }
],
data: result.claims.map(c => ({
issueId: c.issueId,
claimant: c.claimantId || output.dim('unclaimed'),
type: c.claimantType ? formatClaimantType(c.claimantType) : '-',
status: formatClaimStatus(c.status),
progress: formatProgress(c.progress),
time: formatTimeRemaining(c.expiresAt)
}))
});
output.writeln();
output.printInfo(`Showing ${result.claims.length} of ${result.total} issues (${result.available} available)`);
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to list issues: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// ============================================
// Claim Subcommand
// ============================================
const claimCommand: Command = {
name: 'claim',
description: 'Claim an issue to work on',
options: [
{
name: 'as',
description: 'Claim as specific identity (agent:id or human:id)',
type: 'string'
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const issueId = ctx.args[0];
const asIdentity = ctx.flags.as as string | undefined;
if (!issueId) {
output.printError('Issue ID is required');
output.printInfo('Usage: claude-flow issues claim <issueId>');
return { success: false, exitCode: 1 };
}
let claimantId = 'current-agent';
let claimantType: ClaimantType = 'agent';
if (asIdentity) {
const parsed = parseTarget(asIdentity);
claimantId = parsed.id;
claimantType = parsed.type;
}
output.writeln();
output.printInfo(`Claiming issue ${output.highlight(issueId)}...`);
try {
const result = await callMCPTool<Claim>('claims/claim', {
issueId,
claimantId,
claimantType
});
output.writeln();
output.printSuccess(`Issue ${issueId} claimed successfully`);
output.writeln();
output.printTable({
columns: [
{ key: 'property', header: 'Property', width: 15 },
{ key: 'value', header: 'Value', width: 35 }
],
data: [
{ property: 'Issue ID', value: result.issueId },
{ property: 'Claimant', value: result.claimantId },
{ property: 'Type', value: formatClaimantType(result.claimantType) },
{ property: 'Status', value: formatClaimStatus(result.status) },
{ property: 'Claimed At', value: new Date(result.claimedAt).toLocaleString() },
{ property: 'Expires At', value: result.expiresAt ? new Date(result.expiresAt).toLocaleString() : 'N/A' }
]
});
if (ctx.flags.format === 'json') {
output.printJson(result);
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to claim issue: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// ============================================
// Release Subcommand
// ============================================
const releaseCommand: Command = {
name: 'release',
aliases: ['unclaim'],
description: 'Release a claim on an issue',
options: [
{
name: 'force',
short: 'f',
description: 'Force release without confirmation',
type: 'boolean',
default: false
},
{
name: 'reason',
short: 'r',
description: 'Reason for releasing the claim',
type: 'string'
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const issueId = ctx.args[0];
const force = ctx.flags.force as boolean;
const reason = ctx.flags.reason as string | undefined;
if (!issueId) {
output.printError('Issue ID is required');
output.printInfo('Usage: claude-flow issues release <issueId>');
return { success: false, exitCode: 1 };
}
if (!force && ctx.interactive) {
const confirmed = await confirm({
message: `Release your claim on issue ${issueId}?`,
default: false
});
if (!confirmed) {
output.printInfo('Operation cancelled');
return { success: true };
}
}
output.writeln();
output.printInfo(`Releasing claim on issue ${output.highlight(issueId)}...`);
try {
await callMCPTool<void>('claims/release', {
issueId,
reason: reason || 'Released by user via CLI'
});
output.writeln();
output.printSuccess(`Claim on issue ${issueId} released`);
if (reason) {
output.printInfo(`Reason: ${reason}`);
}
return { success: true };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to release claim: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// ============================================
// Handoff Subcommand
// ============================================
const handoffCommand: Command = {
name: 'handoff',
description: 'Request handoff of an issue to another agent or human',
options: [
{
name: 'to',
short: 't',
description: 'Target for handoff (agent:id or human:id)',
type: 'string',
required: true
},
{
name: 'reason',
short: 'r',
description: 'Reason for handoff',
type: 'string'
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const issueId = ctx.args[0];
let target = ctx.flags.to as string;
const reason = ctx.flags.reason as string | undefined;
if (!issueId) {
output.printError('Issue ID is required');
output.printInfo('Usage: claude-flow issues handoff <issueId> --to <target>');
return { success: false, exitCode: 1 };
}
if (!target && ctx.interactive) {
target = await input({
message: 'Handoff to (agent:id or human:id):',
validate: (v) => {
try {
parseTarget(v);
return true;
} catch {
return 'Invalid format. Use agent:<id> or human:<id>';
}
}
});
}
if (!target) {
output.printError('Target is required. Use --to flag (e.g., --to agent:coder-1)');
return { success: false, exitCode: 1 };
}
const parsedTarget = parseTarget(target);
output.writeln();
output.printInfo(`Requesting handoff of ${output.highlight(issueId)} to ${output.highlight(target)}...`);
try {
const result = await callMCPTool<HandoffRequest>('claims/handoff', {
issueId,
targetId: parsedTarget.id,
targetType: parsedTarget.type,
reason
});
output.writeln();
output.printSuccess(`Handoff requested for issue ${issueId}`);
output.writeln();
output.printTable({
columns: [
{ key: 'property', header: 'Property', width: 15 },
{ key: 'value', header: 'Value', width: 35 }
],
data: [
{ property: 'Issue ID', value: result.issueId },
{ property: 'From', value: result.fromId },
{ property: 'To', value: `${result.toType}:${result.toId}` },
{ property: 'Status', value: output.warning(result.status) },
{ property: 'Requested At', value: new Date(result.requestedAt).toLocaleString() }
]
});
if (ctx.flags.format === 'json') {
output.printJson(result);
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to request handoff: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// ============================================
// Status Subcommand
// ============================================
const statusCommand: Command = {
name: 'status',
description: 'Update or view issue claim status',
options: [
{
name: 'blocked',
short: 'b',
description: 'Mark issue as blocked with reason',
type: 'string'
},
{
name: 'review-requested',
short: 'r',
description: 'Request review for the issue',
type: 'boolean',
default: false
},
{
name: 'active',
short: 'a',
description: 'Mark issue as active (unblock)',
type: 'boolean',
default: false
},
{
name: 'progress',
short: 'p',
description: 'Update progress percentage (0-100)',
type: 'number'
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const issueId = ctx.args[0];
if (!issueId) {
output.printError('Issue ID is required');
output.printInfo('Usage: claude-flow issues status <issueId> [options]');
return { success: false, exitCode: 1 };
}
const blocked = ctx.flags.blocked as string | undefined;
const reviewRequested = ctx.flags['review-requested'] as boolean;
const active = ctx.flags.active as boolean;
const progress = ctx.flags.progress as number | undefined;
// If no update flags, show current status
if (!blocked && !reviewRequested && !active && progress === undefined) {
try {
const result = await callMCPTool<Claim>('claims/status', { issueId });
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.printBox(
[
`Claimant: ${result.claimantId || 'unclaimed'}`,
`Type: ${formatClaimantType(result.claimantType)}`,
`Status: ${formatClaimStatus(result.status)}`,
`Progress: ${formatProgress(result.progress)}`,
'',
`Claimed At: ${result.claimedAt ? new Date(result.claimedAt).toLocaleString() : 'N/A'}`,
`Expires At: ${result.expiresAt ? new Date(result.expiresAt).toLocaleString() : 'N/A'}`,
'',
result.blockedReason ? `Blocked: ${result.blockedReason}` : '',
result.stealableReason ? `Stealable: ${result.stealableReason}` : ''
].filter(Boolean).join('\n'),
`Issue: ${issueId}`
);
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to get status: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
// Update status
let newStatus: ClaimStatus | undefined;
let reason: string | undefined;
if (blocked) {
newStatus = 'blocked';
reason = blocked;
} else if (reviewRequested) {
newStatus = 'review-requested';
} else if (active) {
newStatus = 'active';
}
output.writeln();
output.printInfo(`Updating issue ${output.highlight(issueId)}...`);
try {
const result = await callMCPTool<Claim>('claims/update', {
issueId,
status: newStatus,
reason,
progress
});
output.writeln();
output.printSuccess(`Issue ${issueId} updated`);
output.writeln();
const updates: Array<{ property: string; value: string }> = [];
if (newStatus) {
updates.push({ property: 'Status', value: formatClaimStatus(result.status) });
}
if (reason) {
updates.push({ property: 'Reason', value: reason });
}
if (progress !== undefined) {
updates.push({ property: 'Progress', value: formatProgress(result.progress) });
}
output.printTable({
columns: [
{ key: 'property', header: 'Property', width: 15 },
{ key: 'value', header: 'Value', width: 35 }
],
data: updates
});
if (ctx.flags.format === 'json') {
output.printJson(result);
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to update status: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// ============================================
// Board Subcommand
// ============================================
const boardCommand: Command = {
name: 'board',
description: 'View who is working on what',
options: [
{
name: 'all',
short: 'a',
description: 'Show all issues including completed',
type: 'boolean',
default: false
},
{
name: 'group',
short: 'g',
description: 'Group by claimant type',
type: 'boolean',
default: false
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const showAll = ctx.flags.all as boolean;
const groupBy = ctx.flags.group as boolean;
try {
const result = await callMCPTool<{
claims: Claim[];
stats: {
totalClaimed: number;
totalAvailable: number;
agentClaims: number;
humanClaims: number;
};
}>('claims/board', {
includeCompleted: showAll
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.writeln(output.bold('Issue Claims Board'));
output.writeln();
// Stats summary
output.printList([
`Total Claimed: ${output.highlight(String(result.stats.totalClaimed))}`,
`Available: ${output.success(String(result.stats.totalAvailable))}`,
`By Agents: ${output.info(String(result.stats.agentClaims))}`,
`By Humans: ${output.highlight(String(result.stats.humanClaims))}`
]);
output.writeln();
if (result.claims.length === 0) {
output.printInfo('No active claims');
return { success: true, data: result };
}
if (groupBy) {
// Group by claimant type
const agents = result.claims.filter(c => c.claimantType === 'agent');
const humans = result.claims.filter(c => c.claimantType === 'human');
if (agents.length > 0) {
output.writeln(output.bold('Agent Claims'));
printBoardTable(agents);
output.writeln();
}
if (humans.length > 0) {
output.writeln(output.bold('Human Claims'));
printBoardTable(humans);
output.writeln();
}
} else {
printBoardTable(result.claims);
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to load board: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
function printBoardTable(claims: Claim[]): void {
output.printTable({
columns: [
{ key: 'issue', header: 'Issue', width: 12 },
{ key: 'claimant', header: 'Claimant', width: 15 },
{ key: 'type', header: 'Type', width: 8 },
{ key: 'status', header: 'Status', width: 16 },
{ key: 'progress', header: 'Progress', width: 10 },
{ key: 'time', header: 'Time', width: 12 }
],
data: claims.map(c => ({
issue: c.issueId,
claimant: c.claimantId,
type: formatClaimantType(c.claimantType),
status: formatClaimStatus(c.status),
progress: formatProgress(c.progress),
time: formatTimeRemaining(c.expiresAt)
}))
});
}
// ============================================
// Work Stealing Commands
// ============================================
const stealableCommand: Command = {
name: 'stealable',
description: 'List stealable issues',
options: [
{
name: 'limit',
short: 'l',
description: 'Maximum number of issues to show',
type: 'number',
default: 20
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const limit = ctx.flags.limit as number;
try {
const result = await callMCPTool<{
claims: Claim[];
total: number;
}>('claims/stealable', { limit });
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.writeln(output.bold('Stealable Issues'));
output.writeln();
if (result.claims.length === 0) {
output.printInfo('No stealable issues available');
return { success: true, data: result };
}
output.printTable({
columns: [
{ key: 'issue', header: 'Issue', width: 12 },
{ key: 'claimant', header: 'Current Owner', width: 15 },
{ key: 'progress', header: 'Progress', width: 10 },
{ key: 'reason', header: 'Stealable Reason', width: 30 }
],
data: result.claims.map(c => ({
issue: c.issueId,
claimant: c.claimantId,
progress: formatProgress(c.progress),
reason: c.stealableReason || output.dim('No reason provided')
}))
});
output.writeln();
output.printInfo(`Showing ${result.claims.length} of ${result.total} stealable issues`);
output.writeln();
output.printInfo('Use "claude-flow issues steal <issueId>" to take over an issue');
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to list stealable issues: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
const stealCommand: Command = {
name: 'steal',
description: 'Steal an issue from another agent',
options: [
{
name: 'force',
short: 'f',
description: 'Force steal without confirmation',
type: 'boolean',
default: false
},
{
name: 'reason',
short: 'r',
description: 'Reason for stealing',
type: 'string'
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const issueId = ctx.args[0];
const force = ctx.flags.force as boolean;
const reason = ctx.flags.reason as string | undefined;
if (!issueId) {
output.printError('Issue ID is required');
output.printInfo('Usage: claude-flow issues steal <issueId>');
return { success: false, exitCode: 1 };
}
if (!force && ctx.interactive) {
output.writeln();
output.printWarning('Work stealing should be used responsibly.');
output.printInfo('This action will reassign the issue to you.');
output.writeln();
const confirmed = await confirm({
message: `Steal issue ${issueId}?`,
default: false
});
if (!confirmed) {
output.printInfo('Operation cancelled');
return { success: true };
}
}
output.writeln();
output.printInfo(`Stealing issue ${output.highlight(issueId)}...`);
try {
const result = await callMCPTool<Claim>('claims/steal', {
issueId,
reason
});
output.writeln();
output.printSuccess(`Issue ${issueId} stolen successfully`);
output.writeln();
output.printTable({
columns: [
{ key: 'property', header: 'Property', width: 15 },
{ key: 'value', header: 'Value', width: 35 }
],
data: [
{ property: 'Issue ID', value: result.issueId },
{ property: 'New Claimant', value: result.claimantId },
{ property: 'Status', value: formatClaimStatus(result.status) },
{ property: 'Progress', value: formatProgress(result.progress) }
]
});
if (ctx.flags.format === 'json') {
output.printJson(result);
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to steal issue: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
const markStealableCommand: Command = {
name: 'mark-stealable',
description: 'Mark my claim as stealable',
options: [
{
name: 'reason',
short: 'r',
description: 'Reason for marking as stealable',
type: 'string'
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const issueId = ctx.args[0];
let reason = ctx.flags.reason as string | undefined;
if (!issueId) {
output.printError('Issue ID is required');
output.printInfo('Usage: claude-flow issues mark-stealable <issueId>');
return { success: false, exitCode: 1 };
}
if (!reason && ctx.interactive) {
reason = await input({
message: 'Reason for marking as stealable (optional):',
default: ''
});
}
output.writeln();
output.printInfo(`Marking issue ${output.highlight(issueId)} as stealable...`);
try {
const result = await callMCPTool<Claim>('claims/mark-stealable', {
issueId,
reason: reason || undefined
});
output.writeln();
output.printSuccess(`Issue ${issueId} marked as stealable`);
if (reason) {
output.printInfo(`Reason: ${reason}`);
}
output.writeln();
output.printWarning('Other agents can now claim this issue');
if (ctx.flags.format === 'json') {
output.printJson(result);
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to mark as stealable: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
const contestCommand: Command = {
name: 'contest',
description: 'Contest a steal action',
options: [
{
name: 'reason',
short: 'r',
description: 'Reason for contesting (required)',
type: 'string',
required: true
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const issueId = ctx.args[0];
let reason = ctx.flags.reason as string;
if (!issueId) {
output.printError('Issue ID is required');
output.printInfo('Usage: claude-flow issues contest <issueId> --reason "..."');
return { success: false, exitCode: 1 };
}
if (!reason && ctx.interactive) {
reason = await input({
message: 'Reason for contesting (required):',
validate: (v) => v.length > 0 || 'Reason is required'
});
}
if (!reason) {
output.printError('Reason is required for contesting');
return { success: false, exitCode: 1 };
}
output.writeln();
output.printInfo(`Contesting steal on issue ${output.highlight(issueId)}...`);
try {
const result = await callMCPTool<ContestResult>('claims/contest', {
issueId,
reason
});
output.writeln();
switch (result.resolution) {
case 'steal-reverted':
output.printSuccess('Contest successful - steal reverted');
output.printInfo(`Issue ${issueId} returned to original claimant: ${result.originalClaimantId}`);
break;
case 'steal-upheld':
output.printWarning('Contest denied - steal upheld');
output.printInfo(`Issue ${issueId} remains with: ${result.contesterId}`);
break;
case 'pending-review':
output.printWarning('Contest submitted for review');
output.printInfo('A coordinator will review this contest');
break;
}
if (ctx.flags.format === 'json') {
output.printJson(result);
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to contest steal: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// ============================================
// Load Balancing Commands
// ============================================
const loadCommand: Command = {
name: 'load',
description: 'View agent load distribution',
options: [
{
name: 'agent',
short: 'a',
description: 'View specific agent load',
type: 'string'
},
{
name: 'sort',
short: 's',
description: 'Sort by field',
type: 'string',
choices: ['utilization', 'issues', 'agent'],
default: 'utilization'
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const agentId = ctx.flags.agent as string | undefined;
const sortBy = ctx.flags.sort as string;
try {
const result = await callMCPTool<{
agents: AgentLoad[];
summary: {
totalAgents: number;
totalIssues: number;
avgUtilization: number;
overloadedCount: number;
idleCount: number;
};
}>('claims/load', {
agentId,
sortBy
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.writeln(output.bold('Agent Load Distribution'));
output.writeln();
// Summary
output.printList([
`Total Agents: ${result.summary.totalAgents}`,
`Active Issues: ${result.summary.totalIssues}`,
`Avg Utilization: ${result.summary.avgUtilization.toFixed(1)}%`,
`Overloaded: ${output.error(String(result.summary.overloadedCount))}`,
`Idle: ${output.dim(String(result.summary.idleCount))}`
]);
output.writeln();
if (agentId) {
// Single agent detail
const agent = result.agents[0];
if (!agent) {
output.printError(`Agent ${agentId} not found`);
return { success: false, exitCode: 1 };
}
output.printBox(
[
`Type: ${agent.agentType}`,
`Status: ${formatAgentStatus(agent.status)}`,
`Active Issues: ${agent.activeIssues}`,
`Capacity: ${agent.totalCapacity}`,
`Utilization: ${output.progressBar(agent.utilizationPercent, 100, 30)}`,
`Avg Completion: ${agent.avgCompletionTime}`
].join('\n'),
`Agent: ${agent.agentId}`
);
} else {
// All agents table
output.printTable({
columns: [
{ key: 'agent', header: 'Agent', width: 15 },
{ key: 'type', header: 'Type', width: 12 },
{ key: 'issues', header: 'Issues', width: 8, align: 'right' },
{ key: 'capacity', header: 'Cap', width: 6, align: 'right' },
{ key: 'utilization', header: 'Utilization', width: 15 },
{ key: 'status', header: 'Status', width: 12 }
],
data: result.agents.map(a => ({
agent: a.agentId,
type: a.agentType,
issues: a.activeIssues,
capacity: a.totalCapacity,
utilization: `${a.utilizationPercent.toFixed(0)}%`,
status: formatAgentStatus(a.status)
}))
});
}
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed to get load info: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
const rebalanceCommand: Command = {
name: 'rebalance',
description: 'Trigger swarm rebalancing',
options: [
{
name: 'dry-run',
short: 'd',
description: 'Preview rebalancing without making changes',
type: 'boolean',
default: false
},
{
name: 'force',
short: 'f',
description: 'Force rebalancing without confirmation',
type: 'boolean',
default: false
},
{
name: 'threshold',
short: 't',
description: 'Utilization threshold for rebalancing (0-100)',
type: 'number',
default: 80
}
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const dryRun = ctx.flags['dry-run'] as boolean;
const force = ctx.flags.force as boolean;
const threshold = ctx.flags.threshold as number;
if (!dryRun && !force && ctx.interactive) {
output.writeln();
output.printWarning('This will reassign issues between agents to balance load.');
const confirmed = await confirm({
message: 'Proceed with rebalancing?',
default: false
});
if (!confirmed) {
output.printInfo('Operation cancelled');
return { success: true };
}
}
output.writeln();
if (dryRun) {
output.printInfo('Analyzing rebalancing options (dry run)...');
} else {
output.printInfo('Rebalancing swarm workload...');
}
const spinner = output.createSpinner({ text: 'Calculating optimal distribution...', spinner: 'dots' });
spinner.start();
try {
const result = await callMCPTool<RebalanceResult>('claims/rebalance', {
dryRun,
threshold
});
spinner.stop();
output.writeln();
if (dryRun) {
output.printSuccess('Rebalancing Analysis Complete (Dry Run)');
} else {
output.printSuccess('Rebalancing Complete');
}
output.writeln();
// Summary stats
output.printList([
`Issues to move: ${output.highlight(String(result.moved))}`,
`Issues skipped: ${output.dim(String(result.skipped))}`,
`Mode: ${dryRun ? output.warning('DRY RUN') : output.success('APPLIED')}`
]);
if (result.reassignments.length > 0) {
output.writeln();
output.writeln(output.bold('Reassignments'));
output.printTable({
columns: [
{ key: 'issue', header: 'Issue', width: 12 },
{ key: 'from', header: 'From', width: 15 },
{ key: 'to', header: 'To', width: 15 },
{ key: 'reason', header: 'Reason', width: 30 }
],
data: result.reassignments.map(r => ({
issue: r.issueId,
from: r.fromAgent,
to: r.toAgent,
reason: r.reason
}))
});
if (dryRun) {
output.writeln();
output.printInfo('Run without --dry-run to apply these changes');
}
} else {
output.writeln();
output.printInfo('No reassignments needed - workload is balanced');
}
if (ctx.flags.format === 'json') {
output.printJson(result);
}
return { success: true, data: result };
} catch (error) {
spinner.fail('Rebalancing failed');
if (error instanceof MCPClientError) {
output.printError(`Failed to rebalance: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// ============================================
// Main Issues Command
// ============================================
export const issuesCommand: Command = {
name: 'issues',
description: 'Manage issue claims and work distribution',
subcommands: [
// Core claiming
listCommand,
claimCommand,
releaseCommand,
handoffCommand,
statusCommand,
boardCommand,
// Work stealing
stealableCommand,
stealCommand,
markStealableCommand,
contestCommand,
// Load balancing
loadCommand,
rebalanceCommand
],
options: [],
examples: [
{ command: 'claude-flow issues list --available', description: 'List unclaimed issues' },
{ command: 'claude-flow issues list --mine', description: 'List my claims' },
{ command: 'claude-flow issues claim GH-123', description: 'Claim an issue' },
{ command: 'claude-flow issues release GH-123', description: 'Release a claim' },
{ command: 'claude-flow issues handoff GH-123 --to agent:coder-1', description: 'Request handoff to agent' },
{ command: 'claude-flow issues handoff GH-123 --to human:alice', description: 'Request handoff to human' },
{ command: 'claude-flow issues status GH-123 --blocked "Waiting for API"', description: 'Mark as blocked' },
{ command: 'claude-flow issues status GH-123 --review-requested', description: 'Request review' },
{ command: 'claude-flow issues board', description: 'View who is working on what' },
{ command: 'claude-flow issues stealable', description: 'List stealable issues' },
{ command: 'claude-flow issues steal GH-123', description: 'Steal an issue' },
{ command: 'claude-flow issues mark-stealable GH-123', description: 'Mark my claim as stealable' },
{ command: 'claude-flow issues contest GH-123 -r "I was actively working on it"', description: 'Contest a steal' },
{ command: 'claude-flow issues load', description: 'View agent load distribution' },
{ command: 'claude-flow issues load --agent coder-1', description: 'View specific agent load' },
{ command: 'claude-flow issues rebalance --dry-run', description: 'Preview rebalancing' },
{ command: 'claude-flow issues rebalance', description: 'Trigger swarm rebalancing' }
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
// Show help if no subcommand
output.writeln();
output.writeln(output.bold('Issue Claims Management'));
output.writeln();
output.writeln('Usage: claude-flow issues <subcommand> [options]');
output.writeln();
output.writeln(output.bold('Core Commands'));
output.printList([
`${output.highlight('list')} - List issues (--available, --mine)`,
`${output.highlight('claim')} - Claim an issue to work on`,
`${output.highlight('release')} - Release a claim on an issue`,
`${output.highlight('handoff')} - Request handoff to another agent/human`,
`${output.highlight('status')} - Update or view issue claim status`,
`${output.highlight('board')} - View who is working on what`
]);
output.writeln();
output.writeln(output.bold('Work Stealing Commands'));
output.printList([
`${output.highlight('stealable')} - List stealable issues`,
`${output.highlight('steal')} - Steal an issue from another agent`,
`${output.highlight('mark-stealable')} - Mark my claim as stealable`,
`${output.highlight('contest')} - Contest a steal action`
]);
output.writeln();
output.writeln(output.bold('Load Balancing Commands'));
output.printList([
`${output.highlight('load')} - View agent load distribution`,
`${output.highlight('rebalance')} - Trigger swarm rebalancing`
]);
output.writeln();
output.writeln('Run "claude-flow issues <subcommand> --help" for subcommand help');
return { success: true };
}
};
// ============================================
// Factory Function (for dependency injection)
// ============================================
/**
* Create issues command with injected services
* This allows for testing with mock services
*/
export function createIssuesCommand(services: ClaimServices): Command {
// The command structure remains the same, but actions would use
// the injected services instead of callMCPTool
// For now, we return the default command which uses MCP tools
return issuesCommand;
}
export default issuesCommand;