@sparkleideas/claims 3.5.2-patch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1459 @@
1
+ // @ts-nocheck - CLI integration requires the full @sparkleideas/cli package
2
+ /**
3
+ * V3 CLI Claims Command
4
+ * Issue claiming and work distribution management
5
+ *
6
+ * Implements:
7
+ * - Core claiming commands (list, claim, release, handoff, status)
8
+ * - Work stealing commands (stealable, steal, mark-stealable, contest)
9
+ * - Load balancing commands (load, rebalance)
10
+ */
11
+
12
+ import type { Command, CommandContext, CommandResult } from './cli-types.js';
13
+ import { output, select, confirm, input, callMCPTool, MCPClientError } from './cli-types.js';
14
+
15
+ // ============================================
16
+ // Types
17
+ // ============================================
18
+
19
+ export interface ClaimServices {
20
+ claimIssue: (issueId: string, claimantId: string, claimantType: ClaimantType) => Promise<Claim>;
21
+ releaseClaim: (issueId: string, claimantId: string) => Promise<void>;
22
+ requestHandoff: (issueId: string, targetId: string, targetType: ClaimantType) => Promise<HandoffRequest>;
23
+ updateStatus: (issueId: string, status: ClaimStatus, reason?: string) => Promise<Claim>;
24
+ listClaims: (filter?: ClaimFilter) => Promise<Claim[]>;
25
+ listStealable: () => Promise<Claim[]>;
26
+ stealIssue: (issueId: string, stealerId: string) => Promise<Claim>;
27
+ markStealable: (issueId: string, reason?: string) => Promise<Claim>;
28
+ contestSteal: (issueId: string, contesterId: string, reason: string) => Promise<ContestResult>;
29
+ getAgentLoad: (agentId?: string) => Promise<AgentLoad[]>;
30
+ rebalance: (dryRun?: boolean) => Promise<RebalanceResult>;
31
+ }
32
+
33
+ export type ClaimantType = 'agent' | 'human';
34
+ export type ClaimStatus = 'active' | 'blocked' | 'review-requested' | 'stealable' | 'completed';
35
+
36
+ export interface Claim {
37
+ issueId: string;
38
+ claimantId: string;
39
+ claimantType: ClaimantType;
40
+ status: ClaimStatus;
41
+ progress: number;
42
+ claimedAt: string;
43
+ expiresAt?: string;
44
+ blockedReason?: string;
45
+ stealableReason?: string;
46
+ }
47
+
48
+ export interface ClaimFilter {
49
+ claimantId?: string;
50
+ status?: ClaimStatus;
51
+ available?: boolean;
52
+ }
53
+
54
+ export interface HandoffRequest {
55
+ issueId: string;
56
+ fromId: string;
57
+ toId: string;
58
+ toType: ClaimantType;
59
+ requestedAt: string;
60
+ status: 'pending' | 'accepted' | 'rejected';
61
+ }
62
+
63
+ export interface ContestResult {
64
+ issueId: string;
65
+ contesterId: string;
66
+ originalClaimantId: string;
67
+ resolution: 'steal-reverted' | 'steal-upheld' | 'pending-review';
68
+ reason?: string;
69
+ }
70
+
71
+ export interface AgentLoad {
72
+ agentId: string;
73
+ agentType: string;
74
+ activeIssues: number;
75
+ totalCapacity: number;
76
+ utilizationPercent: number;
77
+ avgCompletionTime: string;
78
+ status: 'healthy' | 'overloaded' | 'idle';
79
+ }
80
+
81
+ export interface RebalanceResult {
82
+ moved: number;
83
+ reassignments: Array<{
84
+ issueId: string;
85
+ fromAgent: string;
86
+ toAgent: string;
87
+ reason: string;
88
+ }>;
89
+ skipped: number;
90
+ dryRun: boolean;
91
+ }
92
+
93
+ // ============================================
94
+ // Formatting Helpers
95
+ // ============================================
96
+
97
+ function formatClaimStatus(status: ClaimStatus): string {
98
+ switch (status) {
99
+ case 'active':
100
+ return output.success(status);
101
+ case 'blocked':
102
+ return output.error(status);
103
+ case 'review-requested':
104
+ return output.warning(status);
105
+ case 'stealable':
106
+ return output.warning(status);
107
+ case 'completed':
108
+ return output.dim(status);
109
+ default:
110
+ return status;
111
+ }
112
+ }
113
+
114
+ function formatClaimantType(type: ClaimantType): string {
115
+ switch (type) {
116
+ case 'agent':
117
+ return output.info(type);
118
+ case 'human':
119
+ return output.highlight(type);
120
+ default:
121
+ return type;
122
+ }
123
+ }
124
+
125
+ function formatAgentStatus(status: 'healthy' | 'overloaded' | 'idle'): string {
126
+ switch (status) {
127
+ case 'healthy':
128
+ return output.success(status);
129
+ case 'overloaded':
130
+ return output.error(status);
131
+ case 'idle':
132
+ return output.dim(status);
133
+ default:
134
+ return status;
135
+ }
136
+ }
137
+
138
+ function formatProgress(progress: number): string {
139
+ if (progress >= 75) {
140
+ return output.success(`${progress}%`);
141
+ } else if (progress >= 25) {
142
+ return output.warning(`${progress}%`);
143
+ }
144
+ return output.dim(`${progress}%`);
145
+ }
146
+
147
+ function formatTimeRemaining(expiresAt?: string): string {
148
+ if (!expiresAt) return output.dim('N/A');
149
+
150
+ const expiry = new Date(expiresAt);
151
+ const now = new Date();
152
+ const diffMs = expiry.getTime() - now.getTime();
153
+
154
+ if (diffMs <= 0) {
155
+ return output.error('EXPIRED');
156
+ }
157
+
158
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
159
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
160
+
161
+ if (hours < 1) {
162
+ return output.warning(`${minutes}m`);
163
+ } else if (hours < 4) {
164
+ return output.warning(`${hours}h ${minutes}m`);
165
+ }
166
+
167
+ return output.dim(`${hours}h ${minutes}m`);
168
+ }
169
+
170
+ function parseTarget(target: string): { id: string; type: ClaimantType } {
171
+ // Format: agent:coder-1 or human:alice
172
+ const [type, id] = target.split(':');
173
+ if (!type || !id || (type !== 'agent' && type !== 'human')) {
174
+ throw new Error(`Invalid target format: ${target}. Use agent:<id> or human:<id>`);
175
+ }
176
+ return { id, type: type as ClaimantType };
177
+ }
178
+
179
+ // ============================================
180
+ // List Subcommand
181
+ // ============================================
182
+
183
+ const listCommand: Command = {
184
+ name: 'list',
185
+ aliases: ['ls'],
186
+ description: 'List issues',
187
+ options: [
188
+ {
189
+ name: 'available',
190
+ short: 'a',
191
+ description: 'Show only unclaimed issues',
192
+ type: 'boolean',
193
+ default: false
194
+ },
195
+ {
196
+ name: 'mine',
197
+ short: 'm',
198
+ description: 'Show only my claims',
199
+ type: 'boolean',
200
+ default: false
201
+ },
202
+ {
203
+ name: 'status',
204
+ short: 's',
205
+ description: 'Filter by status',
206
+ type: 'string',
207
+ choices: ['active', 'blocked', 'review-requested', 'stealable', 'completed']
208
+ },
209
+ {
210
+ name: 'limit',
211
+ short: 'l',
212
+ description: 'Maximum number of issues to show',
213
+ type: 'number',
214
+ default: 20
215
+ }
216
+ ],
217
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
218
+ const available = ctx.flags.available as boolean;
219
+ const mine = ctx.flags.mine as boolean;
220
+ const status = ctx.flags.status as ClaimStatus | undefined;
221
+ const limit = ctx.flags.limit as number;
222
+
223
+ try {
224
+ const result = await callMCPTool<{
225
+ claims: Claim[];
226
+ total: number;
227
+ available: number;
228
+ }>('claims/list', {
229
+ available,
230
+ mine,
231
+ status,
232
+ limit
233
+ });
234
+
235
+ if (ctx.flags.format === 'json') {
236
+ output.printJson(result);
237
+ return { success: true, data: result };
238
+ }
239
+
240
+ output.writeln();
241
+
242
+ if (available) {
243
+ output.writeln(output.bold('Available Issues (Unclaimed)'));
244
+ } else if (mine) {
245
+ output.writeln(output.bold('My Claims'));
246
+ } else {
247
+ output.writeln(output.bold('All Claims'));
248
+ }
249
+
250
+ output.writeln();
251
+
252
+ if (result.claims.length === 0) {
253
+ if (available) {
254
+ output.printInfo('No unclaimed issues available');
255
+ } else if (mine) {
256
+ output.printInfo('You have no active claims');
257
+ } else {
258
+ output.printInfo('No claims found matching criteria');
259
+ }
260
+ return { success: true, data: result };
261
+ }
262
+
263
+ output.printTable({
264
+ columns: [
265
+ { key: 'issueId', header: 'Issue', width: 12 },
266
+ { key: 'claimant', header: 'Claimant', width: 15 },
267
+ { key: 'type', header: 'Type', width: 8 },
268
+ { key: 'status', header: 'Status', width: 16 },
269
+ { key: 'progress', header: 'Progress', width: 10 },
270
+ { key: 'time', header: 'Time Left', width: 12 }
271
+ ],
272
+ data: result.claims.map(c => ({
273
+ issueId: c.issueId,
274
+ claimant: c.claimantId || output.dim('unclaimed'),
275
+ type: c.claimantType ? formatClaimantType(c.claimantType) : '-',
276
+ status: formatClaimStatus(c.status),
277
+ progress: formatProgress(c.progress),
278
+ time: formatTimeRemaining(c.expiresAt)
279
+ }))
280
+ });
281
+
282
+ output.writeln();
283
+ output.printInfo(`Showing ${result.claims.length} of ${result.total} issues (${result.available} available)`);
284
+
285
+ return { success: true, data: result };
286
+ } catch (error) {
287
+ if (error instanceof MCPClientError) {
288
+ output.printError(`Failed to list issues: ${error.message}`);
289
+ } else {
290
+ output.printError(`Unexpected error: ${String(error)}`);
291
+ }
292
+ return { success: false, exitCode: 1 };
293
+ }
294
+ }
295
+ };
296
+
297
+ // ============================================
298
+ // Claim Subcommand
299
+ // ============================================
300
+
301
+ const claimCommand: Command = {
302
+ name: 'claim',
303
+ description: 'Claim an issue to work on',
304
+ options: [
305
+ {
306
+ name: 'as',
307
+ description: 'Claim as specific identity (agent:id or human:id)',
308
+ type: 'string'
309
+ }
310
+ ],
311
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
312
+ const issueId = ctx.args[0];
313
+ const asIdentity = ctx.flags.as as string | undefined;
314
+
315
+ if (!issueId) {
316
+ output.printError('Issue ID is required');
317
+ output.printInfo('Usage: @sparkleideas/claude-flow issues claim <issueId>');
318
+ return { success: false, exitCode: 1 };
319
+ }
320
+
321
+ let claimantId = 'current-agent';
322
+ let claimantType: ClaimantType = 'agent';
323
+
324
+ if (asIdentity) {
325
+ const parsed = parseTarget(asIdentity);
326
+ claimantId = parsed.id;
327
+ claimantType = parsed.type;
328
+ }
329
+
330
+ output.writeln();
331
+ output.printInfo(`Claiming issue ${output.highlight(issueId)}...`);
332
+
333
+ try {
334
+ const result = await callMCPTool<Claim>('claims/claim', {
335
+ issueId,
336
+ claimantId,
337
+ claimantType
338
+ });
339
+
340
+ output.writeln();
341
+ output.printSuccess(`Issue ${issueId} claimed successfully`);
342
+ output.writeln();
343
+
344
+ output.printTable({
345
+ columns: [
346
+ { key: 'property', header: 'Property', width: 15 },
347
+ { key: 'value', header: 'Value', width: 35 }
348
+ ],
349
+ data: [
350
+ { property: 'Issue ID', value: result.issueId },
351
+ { property: 'Claimant', value: result.claimantId },
352
+ { property: 'Type', value: formatClaimantType(result.claimantType) },
353
+ { property: 'Status', value: formatClaimStatus(result.status) },
354
+ { property: 'Claimed At', value: new Date(result.claimedAt).toLocaleString() },
355
+ { property: 'Expires At', value: result.expiresAt ? new Date(result.expiresAt).toLocaleString() : 'N/A' }
356
+ ]
357
+ });
358
+
359
+ if (ctx.flags.format === 'json') {
360
+ output.printJson(result);
361
+ }
362
+
363
+ return { success: true, data: result };
364
+ } catch (error) {
365
+ if (error instanceof MCPClientError) {
366
+ output.printError(`Failed to claim issue: ${error.message}`);
367
+ } else {
368
+ output.printError(`Unexpected error: ${String(error)}`);
369
+ }
370
+ return { success: false, exitCode: 1 };
371
+ }
372
+ }
373
+ };
374
+
375
+ // ============================================
376
+ // Release Subcommand
377
+ // ============================================
378
+
379
+ const releaseCommand: Command = {
380
+ name: 'release',
381
+ aliases: ['unclaim'],
382
+ description: 'Release a claim on an issue',
383
+ options: [
384
+ {
385
+ name: 'force',
386
+ short: 'f',
387
+ description: 'Force release without confirmation',
388
+ type: 'boolean',
389
+ default: false
390
+ },
391
+ {
392
+ name: 'reason',
393
+ short: 'r',
394
+ description: 'Reason for releasing the claim',
395
+ type: 'string'
396
+ }
397
+ ],
398
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
399
+ const issueId = ctx.args[0];
400
+ const force = ctx.flags.force as boolean;
401
+ const reason = ctx.flags.reason as string | undefined;
402
+
403
+ if (!issueId) {
404
+ output.printError('Issue ID is required');
405
+ output.printInfo('Usage: @sparkleideas/claude-flow issues release <issueId>');
406
+ return { success: false, exitCode: 1 };
407
+ }
408
+
409
+ if (!force && ctx.interactive) {
410
+ const confirmed = await confirm({
411
+ message: `Release your claim on issue ${issueId}?`,
412
+ default: false
413
+ });
414
+
415
+ if (!confirmed) {
416
+ output.printInfo('Operation cancelled');
417
+ return { success: true };
418
+ }
419
+ }
420
+
421
+ output.writeln();
422
+ output.printInfo(`Releasing claim on issue ${output.highlight(issueId)}...`);
423
+
424
+ try {
425
+ await callMCPTool<void>('claims/release', {
426
+ issueId,
427
+ reason: reason || 'Released by user via CLI'
428
+ });
429
+
430
+ output.writeln();
431
+ output.printSuccess(`Claim on issue ${issueId} released`);
432
+
433
+ if (reason) {
434
+ output.printInfo(`Reason: ${reason}`);
435
+ }
436
+
437
+ return { success: true };
438
+ } catch (error) {
439
+ if (error instanceof MCPClientError) {
440
+ output.printError(`Failed to release claim: ${error.message}`);
441
+ } else {
442
+ output.printError(`Unexpected error: ${String(error)}`);
443
+ }
444
+ return { success: false, exitCode: 1 };
445
+ }
446
+ }
447
+ };
448
+
449
+ // ============================================
450
+ // Handoff Subcommand
451
+ // ============================================
452
+
453
+ const handoffCommand: Command = {
454
+ name: 'handoff',
455
+ description: 'Request handoff of an issue to another agent or human',
456
+ options: [
457
+ {
458
+ name: 'to',
459
+ short: 't',
460
+ description: 'Target for handoff (agent:id or human:id)',
461
+ type: 'string',
462
+ required: true
463
+ },
464
+ {
465
+ name: 'reason',
466
+ short: 'r',
467
+ description: 'Reason for handoff',
468
+ type: 'string'
469
+ }
470
+ ],
471
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
472
+ const issueId = ctx.args[0];
473
+ let target = ctx.flags.to as string;
474
+ const reason = ctx.flags.reason as string | undefined;
475
+
476
+ if (!issueId) {
477
+ output.printError('Issue ID is required');
478
+ output.printInfo('Usage: @sparkleideas/claude-flow issues handoff <issueId> --to <target>');
479
+ return { success: false, exitCode: 1 };
480
+ }
481
+
482
+ if (!target && ctx.interactive) {
483
+ target = await input({
484
+ message: 'Handoff to (agent:id or human:id):',
485
+ validate: (v) => {
486
+ try {
487
+ parseTarget(v);
488
+ return true;
489
+ } catch {
490
+ return 'Invalid format. Use agent:<id> or human:<id>';
491
+ }
492
+ }
493
+ });
494
+ }
495
+
496
+ if (!target) {
497
+ output.printError('Target is required. Use --to flag (e.g., --to agent:coder-1)');
498
+ return { success: false, exitCode: 1 };
499
+ }
500
+
501
+ const parsedTarget = parseTarget(target);
502
+
503
+ output.writeln();
504
+ output.printInfo(`Requesting handoff of ${output.highlight(issueId)} to ${output.highlight(target)}...`);
505
+
506
+ try {
507
+ const result = await callMCPTool<HandoffRequest>('claims/handoff', {
508
+ issueId,
509
+ targetId: parsedTarget.id,
510
+ targetType: parsedTarget.type,
511
+ reason
512
+ });
513
+
514
+ output.writeln();
515
+ output.printSuccess(`Handoff requested for issue ${issueId}`);
516
+ output.writeln();
517
+
518
+ output.printTable({
519
+ columns: [
520
+ { key: 'property', header: 'Property', width: 15 },
521
+ { key: 'value', header: 'Value', width: 35 }
522
+ ],
523
+ data: [
524
+ { property: 'Issue ID', value: result.issueId },
525
+ { property: 'From', value: result.fromId },
526
+ { property: 'To', value: `${result.toType}:${result.toId}` },
527
+ { property: 'Status', value: output.warning(result.status) },
528
+ { property: 'Requested At', value: new Date(result.requestedAt).toLocaleString() }
529
+ ]
530
+ });
531
+
532
+ if (ctx.flags.format === 'json') {
533
+ output.printJson(result);
534
+ }
535
+
536
+ return { success: true, data: result };
537
+ } catch (error) {
538
+ if (error instanceof MCPClientError) {
539
+ output.printError(`Failed to request handoff: ${error.message}`);
540
+ } else {
541
+ output.printError(`Unexpected error: ${String(error)}`);
542
+ }
543
+ return { success: false, exitCode: 1 };
544
+ }
545
+ }
546
+ };
547
+
548
+ // ============================================
549
+ // Status Subcommand
550
+ // ============================================
551
+
552
+ const statusCommand: Command = {
553
+ name: 'status',
554
+ description: 'Update or view issue claim status',
555
+ options: [
556
+ {
557
+ name: 'blocked',
558
+ short: 'b',
559
+ description: 'Mark issue as blocked with reason',
560
+ type: 'string'
561
+ },
562
+ {
563
+ name: 'review-requested',
564
+ short: 'r',
565
+ description: 'Request review for the issue',
566
+ type: 'boolean',
567
+ default: false
568
+ },
569
+ {
570
+ name: 'active',
571
+ short: 'a',
572
+ description: 'Mark issue as active (unblock)',
573
+ type: 'boolean',
574
+ default: false
575
+ },
576
+ {
577
+ name: 'progress',
578
+ short: 'p',
579
+ description: 'Update progress percentage (0-100)',
580
+ type: 'number'
581
+ }
582
+ ],
583
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
584
+ const issueId = ctx.args[0];
585
+
586
+ if (!issueId) {
587
+ output.printError('Issue ID is required');
588
+ output.printInfo('Usage: @sparkleideas/claude-flow issues status <issueId> [options]');
589
+ return { success: false, exitCode: 1 };
590
+ }
591
+
592
+ const blocked = ctx.flags.blocked as string | undefined;
593
+ const reviewRequested = ctx.flags['review-requested'] as boolean;
594
+ const active = ctx.flags.active as boolean;
595
+ const progress = ctx.flags.progress as number | undefined;
596
+
597
+ // If no update flags, show current status
598
+ if (!blocked && !reviewRequested && !active && progress === undefined) {
599
+ try {
600
+ const result = await callMCPTool<Claim>('claims/status', { issueId });
601
+
602
+ if (ctx.flags.format === 'json') {
603
+ output.printJson(result);
604
+ return { success: true, data: result };
605
+ }
606
+
607
+ output.writeln();
608
+ output.printBox(
609
+ [
610
+ `Claimant: ${result.claimantId || 'unclaimed'}`,
611
+ `Type: ${formatClaimantType(result.claimantType)}`,
612
+ `Status: ${formatClaimStatus(result.status)}`,
613
+ `Progress: ${formatProgress(result.progress)}`,
614
+ '',
615
+ `Claimed At: ${result.claimedAt ? new Date(result.claimedAt).toLocaleString() : 'N/A'}`,
616
+ `Expires At: ${result.expiresAt ? new Date(result.expiresAt).toLocaleString() : 'N/A'}`,
617
+ '',
618
+ result.blockedReason ? `Blocked: ${result.blockedReason}` : '',
619
+ result.stealableReason ? `Stealable: ${result.stealableReason}` : ''
620
+ ].filter(Boolean).join('\n'),
621
+ `Issue: ${issueId}`
622
+ );
623
+
624
+ return { success: true, data: result };
625
+ } catch (error) {
626
+ if (error instanceof MCPClientError) {
627
+ output.printError(`Failed to get status: ${error.message}`);
628
+ } else {
629
+ output.printError(`Unexpected error: ${String(error)}`);
630
+ }
631
+ return { success: false, exitCode: 1 };
632
+ }
633
+ }
634
+
635
+ // Update status
636
+ let newStatus: ClaimStatus | undefined;
637
+ let reason: string | undefined;
638
+
639
+ if (blocked) {
640
+ newStatus = 'blocked';
641
+ reason = blocked;
642
+ } else if (reviewRequested) {
643
+ newStatus = 'review-requested';
644
+ } else if (active) {
645
+ newStatus = 'active';
646
+ }
647
+
648
+ output.writeln();
649
+ output.printInfo(`Updating issue ${output.highlight(issueId)}...`);
650
+
651
+ try {
652
+ const result = await callMCPTool<Claim>('claims/update', {
653
+ issueId,
654
+ status: newStatus,
655
+ reason,
656
+ progress
657
+ });
658
+
659
+ output.writeln();
660
+ output.printSuccess(`Issue ${issueId} updated`);
661
+ output.writeln();
662
+
663
+ const updates: Array<{ property: string; value: string }> = [];
664
+
665
+ if (newStatus) {
666
+ updates.push({ property: 'Status', value: formatClaimStatus(result.status) });
667
+ }
668
+
669
+ if (reason) {
670
+ updates.push({ property: 'Reason', value: reason });
671
+ }
672
+
673
+ if (progress !== undefined) {
674
+ updates.push({ property: 'Progress', value: formatProgress(result.progress) });
675
+ }
676
+
677
+ output.printTable({
678
+ columns: [
679
+ { key: 'property', header: 'Property', width: 15 },
680
+ { key: 'value', header: 'Value', width: 35 }
681
+ ],
682
+ data: updates
683
+ });
684
+
685
+ if (ctx.flags.format === 'json') {
686
+ output.printJson(result);
687
+ }
688
+
689
+ return { success: true, data: result };
690
+ } catch (error) {
691
+ if (error instanceof MCPClientError) {
692
+ output.printError(`Failed to update status: ${error.message}`);
693
+ } else {
694
+ output.printError(`Unexpected error: ${String(error)}`);
695
+ }
696
+ return { success: false, exitCode: 1 };
697
+ }
698
+ }
699
+ };
700
+
701
+ // ============================================
702
+ // Board Subcommand
703
+ // ============================================
704
+
705
+ const boardCommand: Command = {
706
+ name: 'board',
707
+ description: 'View who is working on what',
708
+ options: [
709
+ {
710
+ name: 'all',
711
+ short: 'a',
712
+ description: 'Show all issues including completed',
713
+ type: 'boolean',
714
+ default: false
715
+ },
716
+ {
717
+ name: 'group',
718
+ short: 'g',
719
+ description: 'Group by claimant type',
720
+ type: 'boolean',
721
+ default: false
722
+ }
723
+ ],
724
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
725
+ const showAll = ctx.flags.all as boolean;
726
+ const groupBy = ctx.flags.group as boolean;
727
+
728
+ try {
729
+ const result = await callMCPTool<{
730
+ claims: Claim[];
731
+ stats: {
732
+ totalClaimed: number;
733
+ totalAvailable: number;
734
+ agentClaims: number;
735
+ humanClaims: number;
736
+ };
737
+ }>('claims/board', {
738
+ includeCompleted: showAll
739
+ });
740
+
741
+ if (ctx.flags.format === 'json') {
742
+ output.printJson(result);
743
+ return { success: true, data: result };
744
+ }
745
+
746
+ output.writeln();
747
+ output.writeln(output.bold('Issue Claims Board'));
748
+ output.writeln();
749
+
750
+ // Stats summary
751
+ output.printList([
752
+ `Total Claimed: ${output.highlight(String(result.stats.totalClaimed))}`,
753
+ `Available: ${output.success(String(result.stats.totalAvailable))}`,
754
+ `By Agents: ${output.info(String(result.stats.agentClaims))}`,
755
+ `By Humans: ${output.highlight(String(result.stats.humanClaims))}`
756
+ ]);
757
+
758
+ output.writeln();
759
+
760
+ if (result.claims.length === 0) {
761
+ output.printInfo('No active claims');
762
+ return { success: true, data: result };
763
+ }
764
+
765
+ if (groupBy) {
766
+ // Group by claimant type
767
+ const agents = result.claims.filter(c => c.claimantType === 'agent');
768
+ const humans = result.claims.filter(c => c.claimantType === 'human');
769
+
770
+ if (agents.length > 0) {
771
+ output.writeln(output.bold('Agent Claims'));
772
+ printBoardTable(agents);
773
+ output.writeln();
774
+ }
775
+
776
+ if (humans.length > 0) {
777
+ output.writeln(output.bold('Human Claims'));
778
+ printBoardTable(humans);
779
+ output.writeln();
780
+ }
781
+ } else {
782
+ printBoardTable(result.claims);
783
+ }
784
+
785
+ return { success: true, data: result };
786
+ } catch (error) {
787
+ if (error instanceof MCPClientError) {
788
+ output.printError(`Failed to load board: ${error.message}`);
789
+ } else {
790
+ output.printError(`Unexpected error: ${String(error)}`);
791
+ }
792
+ return { success: false, exitCode: 1 };
793
+ }
794
+ }
795
+ };
796
+
797
+ function printBoardTable(claims: Claim[]): void {
798
+ output.printTable({
799
+ columns: [
800
+ { key: 'issue', header: 'Issue', width: 12 },
801
+ { key: 'claimant', header: 'Claimant', width: 15 },
802
+ { key: 'type', header: 'Type', width: 8 },
803
+ { key: 'status', header: 'Status', width: 16 },
804
+ { key: 'progress', header: 'Progress', width: 10 },
805
+ { key: 'time', header: 'Time', width: 12 }
806
+ ],
807
+ data: claims.map(c => ({
808
+ issue: c.issueId,
809
+ claimant: c.claimantId,
810
+ type: formatClaimantType(c.claimantType),
811
+ status: formatClaimStatus(c.status),
812
+ progress: formatProgress(c.progress),
813
+ time: formatTimeRemaining(c.expiresAt)
814
+ }))
815
+ });
816
+ }
817
+
818
+ // ============================================
819
+ // Work Stealing Commands
820
+ // ============================================
821
+
822
+ const stealableCommand: Command = {
823
+ name: 'stealable',
824
+ description: 'List stealable issues',
825
+ options: [
826
+ {
827
+ name: 'limit',
828
+ short: 'l',
829
+ description: 'Maximum number of issues to show',
830
+ type: 'number',
831
+ default: 20
832
+ }
833
+ ],
834
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
835
+ const limit = ctx.flags.limit as number;
836
+
837
+ try {
838
+ const result = await callMCPTool<{
839
+ claims: Claim[];
840
+ total: number;
841
+ }>('claims/stealable', { limit });
842
+
843
+ if (ctx.flags.format === 'json') {
844
+ output.printJson(result);
845
+ return { success: true, data: result };
846
+ }
847
+
848
+ output.writeln();
849
+ output.writeln(output.bold('Stealable Issues'));
850
+ output.writeln();
851
+
852
+ if (result.claims.length === 0) {
853
+ output.printInfo('No stealable issues available');
854
+ return { success: true, data: result };
855
+ }
856
+
857
+ output.printTable({
858
+ columns: [
859
+ { key: 'issue', header: 'Issue', width: 12 },
860
+ { key: 'claimant', header: 'Current Owner', width: 15 },
861
+ { key: 'progress', header: 'Progress', width: 10 },
862
+ { key: 'reason', header: 'Stealable Reason', width: 30 }
863
+ ],
864
+ data: result.claims.map(c => ({
865
+ issue: c.issueId,
866
+ claimant: c.claimantId,
867
+ progress: formatProgress(c.progress),
868
+ reason: c.stealableReason || output.dim('No reason provided')
869
+ }))
870
+ });
871
+
872
+ output.writeln();
873
+ output.printInfo(`Showing ${result.claims.length} of ${result.total} stealable issues`);
874
+ output.writeln();
875
+ output.printInfo('Use "@sparkleideas/claude-flow issues steal <issueId>" to take over an issue');
876
+
877
+ return { success: true, data: result };
878
+ } catch (error) {
879
+ if (error instanceof MCPClientError) {
880
+ output.printError(`Failed to list stealable issues: ${error.message}`);
881
+ } else {
882
+ output.printError(`Unexpected error: ${String(error)}`);
883
+ }
884
+ return { success: false, exitCode: 1 };
885
+ }
886
+ }
887
+ };
888
+
889
+ const stealCommand: Command = {
890
+ name: 'steal',
891
+ description: 'Steal an issue from another agent',
892
+ options: [
893
+ {
894
+ name: 'force',
895
+ short: 'f',
896
+ description: 'Force steal without confirmation',
897
+ type: 'boolean',
898
+ default: false
899
+ },
900
+ {
901
+ name: 'reason',
902
+ short: 'r',
903
+ description: 'Reason for stealing',
904
+ type: 'string'
905
+ }
906
+ ],
907
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
908
+ const issueId = ctx.args[0];
909
+ const force = ctx.flags.force as boolean;
910
+ const reason = ctx.flags.reason as string | undefined;
911
+
912
+ if (!issueId) {
913
+ output.printError('Issue ID is required');
914
+ output.printInfo('Usage: @sparkleideas/claude-flow issues steal <issueId>');
915
+ return { success: false, exitCode: 1 };
916
+ }
917
+
918
+ if (!force && ctx.interactive) {
919
+ output.writeln();
920
+ output.printWarning('Work stealing should be used responsibly.');
921
+ output.printInfo('This action will reassign the issue to you.');
922
+ output.writeln();
923
+
924
+ const confirmed = await confirm({
925
+ message: `Steal issue ${issueId}?`,
926
+ default: false
927
+ });
928
+
929
+ if (!confirmed) {
930
+ output.printInfo('Operation cancelled');
931
+ return { success: true };
932
+ }
933
+ }
934
+
935
+ output.writeln();
936
+ output.printInfo(`Stealing issue ${output.highlight(issueId)}...`);
937
+
938
+ try {
939
+ const result = await callMCPTool<Claim>('claims/steal', {
940
+ issueId,
941
+ reason
942
+ });
943
+
944
+ output.writeln();
945
+ output.printSuccess(`Issue ${issueId} stolen successfully`);
946
+ output.writeln();
947
+
948
+ output.printTable({
949
+ columns: [
950
+ { key: 'property', header: 'Property', width: 15 },
951
+ { key: 'value', header: 'Value', width: 35 }
952
+ ],
953
+ data: [
954
+ { property: 'Issue ID', value: result.issueId },
955
+ { property: 'New Claimant', value: result.claimantId },
956
+ { property: 'Status', value: formatClaimStatus(result.status) },
957
+ { property: 'Progress', value: formatProgress(result.progress) }
958
+ ]
959
+ });
960
+
961
+ if (ctx.flags.format === 'json') {
962
+ output.printJson(result);
963
+ }
964
+
965
+ return { success: true, data: result };
966
+ } catch (error) {
967
+ if (error instanceof MCPClientError) {
968
+ output.printError(`Failed to steal issue: ${error.message}`);
969
+ } else {
970
+ output.printError(`Unexpected error: ${String(error)}`);
971
+ }
972
+ return { success: false, exitCode: 1 };
973
+ }
974
+ }
975
+ };
976
+
977
+ const markStealableCommand: Command = {
978
+ name: 'mark-stealable',
979
+ description: 'Mark my claim as stealable',
980
+ options: [
981
+ {
982
+ name: 'reason',
983
+ short: 'r',
984
+ description: 'Reason for marking as stealable',
985
+ type: 'string'
986
+ }
987
+ ],
988
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
989
+ const issueId = ctx.args[0];
990
+ let reason = ctx.flags.reason as string | undefined;
991
+
992
+ if (!issueId) {
993
+ output.printError('Issue ID is required');
994
+ output.printInfo('Usage: @sparkleideas/claude-flow issues mark-stealable <issueId>');
995
+ return { success: false, exitCode: 1 };
996
+ }
997
+
998
+ if (!reason && ctx.interactive) {
999
+ reason = await input({
1000
+ message: 'Reason for marking as stealable (optional):',
1001
+ default: ''
1002
+ });
1003
+ }
1004
+
1005
+ output.writeln();
1006
+ output.printInfo(`Marking issue ${output.highlight(issueId)} as stealable...`);
1007
+
1008
+ try {
1009
+ const result = await callMCPTool<Claim>('claims/mark-stealable', {
1010
+ issueId,
1011
+ reason: reason || undefined
1012
+ });
1013
+
1014
+ output.writeln();
1015
+ output.printSuccess(`Issue ${issueId} marked as stealable`);
1016
+
1017
+ if (reason) {
1018
+ output.printInfo(`Reason: ${reason}`);
1019
+ }
1020
+
1021
+ output.writeln();
1022
+ output.printWarning('Other agents can now claim this issue');
1023
+
1024
+ if (ctx.flags.format === 'json') {
1025
+ output.printJson(result);
1026
+ }
1027
+
1028
+ return { success: true, data: result };
1029
+ } catch (error) {
1030
+ if (error instanceof MCPClientError) {
1031
+ output.printError(`Failed to mark as stealable: ${error.message}`);
1032
+ } else {
1033
+ output.printError(`Unexpected error: ${String(error)}`);
1034
+ }
1035
+ return { success: false, exitCode: 1 };
1036
+ }
1037
+ }
1038
+ };
1039
+
1040
+ const contestCommand: Command = {
1041
+ name: 'contest',
1042
+ description: 'Contest a steal action',
1043
+ options: [
1044
+ {
1045
+ name: 'reason',
1046
+ short: 'r',
1047
+ description: 'Reason for contesting (required)',
1048
+ type: 'string',
1049
+ required: true
1050
+ }
1051
+ ],
1052
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
1053
+ const issueId = ctx.args[0];
1054
+ let reason = ctx.flags.reason as string;
1055
+
1056
+ if (!issueId) {
1057
+ output.printError('Issue ID is required');
1058
+ output.printInfo('Usage: @sparkleideas/claude-flow issues contest <issueId> --reason "..."');
1059
+ return { success: false, exitCode: 1 };
1060
+ }
1061
+
1062
+ if (!reason && ctx.interactive) {
1063
+ reason = await input({
1064
+ message: 'Reason for contesting (required):',
1065
+ validate: (v) => v.length > 0 || 'Reason is required'
1066
+ });
1067
+ }
1068
+
1069
+ if (!reason) {
1070
+ output.printError('Reason is required for contesting');
1071
+ return { success: false, exitCode: 1 };
1072
+ }
1073
+
1074
+ output.writeln();
1075
+ output.printInfo(`Contesting steal on issue ${output.highlight(issueId)}...`);
1076
+
1077
+ try {
1078
+ const result = await callMCPTool<ContestResult>('claims/contest', {
1079
+ issueId,
1080
+ reason
1081
+ });
1082
+
1083
+ output.writeln();
1084
+
1085
+ switch (result.resolution) {
1086
+ case 'steal-reverted':
1087
+ output.printSuccess('Contest successful - steal reverted');
1088
+ output.printInfo(`Issue ${issueId} returned to original claimant: ${result.originalClaimantId}`);
1089
+ break;
1090
+ case 'steal-upheld':
1091
+ output.printWarning('Contest denied - steal upheld');
1092
+ output.printInfo(`Issue ${issueId} remains with: ${result.contesterId}`);
1093
+ break;
1094
+ case 'pending-review':
1095
+ output.printWarning('Contest submitted for review');
1096
+ output.printInfo('A coordinator will review this contest');
1097
+ break;
1098
+ }
1099
+
1100
+ if (ctx.flags.format === 'json') {
1101
+ output.printJson(result);
1102
+ }
1103
+
1104
+ return { success: true, data: result };
1105
+ } catch (error) {
1106
+ if (error instanceof MCPClientError) {
1107
+ output.printError(`Failed to contest steal: ${error.message}`);
1108
+ } else {
1109
+ output.printError(`Unexpected error: ${String(error)}`);
1110
+ }
1111
+ return { success: false, exitCode: 1 };
1112
+ }
1113
+ }
1114
+ };
1115
+
1116
+ // ============================================
1117
+ // Load Balancing Commands
1118
+ // ============================================
1119
+
1120
+ const loadCommand: Command = {
1121
+ name: 'load',
1122
+ description: 'View agent load distribution',
1123
+ options: [
1124
+ {
1125
+ name: 'agent',
1126
+ short: 'a',
1127
+ description: 'View specific agent load',
1128
+ type: 'string'
1129
+ },
1130
+ {
1131
+ name: 'sort',
1132
+ short: 's',
1133
+ description: 'Sort by field',
1134
+ type: 'string',
1135
+ choices: ['utilization', 'issues', 'agent'],
1136
+ default: 'utilization'
1137
+ }
1138
+ ],
1139
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
1140
+ const agentId = ctx.flags.agent as string | undefined;
1141
+ const sortBy = ctx.flags.sort as string;
1142
+
1143
+ try {
1144
+ const result = await callMCPTool<{
1145
+ agents: AgentLoad[];
1146
+ summary: {
1147
+ totalAgents: number;
1148
+ totalIssues: number;
1149
+ avgUtilization: number;
1150
+ overloadedCount: number;
1151
+ idleCount: number;
1152
+ };
1153
+ }>('claims/load', {
1154
+ agentId,
1155
+ sortBy
1156
+ });
1157
+
1158
+ if (ctx.flags.format === 'json') {
1159
+ output.printJson(result);
1160
+ return { success: true, data: result };
1161
+ }
1162
+
1163
+ output.writeln();
1164
+ output.writeln(output.bold('Agent Load Distribution'));
1165
+ output.writeln();
1166
+
1167
+ // Summary
1168
+ output.printList([
1169
+ `Total Agents: ${result.summary.totalAgents}`,
1170
+ `Active Issues: ${result.summary.totalIssues}`,
1171
+ `Avg Utilization: ${result.summary.avgUtilization.toFixed(1)}%`,
1172
+ `Overloaded: ${output.error(String(result.summary.overloadedCount))}`,
1173
+ `Idle: ${output.dim(String(result.summary.idleCount))}`
1174
+ ]);
1175
+
1176
+ output.writeln();
1177
+
1178
+ if (agentId) {
1179
+ // Single agent detail
1180
+ const agent = result.agents[0];
1181
+ if (!agent) {
1182
+ output.printError(`Agent ${agentId} not found`);
1183
+ return { success: false, exitCode: 1 };
1184
+ }
1185
+
1186
+ output.printBox(
1187
+ [
1188
+ `Type: ${agent.agentType}`,
1189
+ `Status: ${formatAgentStatus(agent.status)}`,
1190
+ `Active Issues: ${agent.activeIssues}`,
1191
+ `Capacity: ${agent.totalCapacity}`,
1192
+ `Utilization: ${output.progressBar(agent.utilizationPercent, 100, 30)}`,
1193
+ `Avg Completion: ${agent.avgCompletionTime}`
1194
+ ].join('\n'),
1195
+ `Agent: ${agent.agentId}`
1196
+ );
1197
+ } else {
1198
+ // All agents table
1199
+ output.printTable({
1200
+ columns: [
1201
+ { key: 'agent', header: 'Agent', width: 15 },
1202
+ { key: 'type', header: 'Type', width: 12 },
1203
+ { key: 'issues', header: 'Issues', width: 8, align: 'right' },
1204
+ { key: 'capacity', header: 'Cap', width: 6, align: 'right' },
1205
+ { key: 'utilization', header: 'Utilization', width: 15 },
1206
+ { key: 'status', header: 'Status', width: 12 }
1207
+ ],
1208
+ data: result.agents.map(a => ({
1209
+ agent: a.agentId,
1210
+ type: a.agentType,
1211
+ issues: a.activeIssues,
1212
+ capacity: a.totalCapacity,
1213
+ utilization: `${a.utilizationPercent.toFixed(0)}%`,
1214
+ status: formatAgentStatus(a.status)
1215
+ }))
1216
+ });
1217
+ }
1218
+
1219
+ return { success: true, data: result };
1220
+ } catch (error) {
1221
+ if (error instanceof MCPClientError) {
1222
+ output.printError(`Failed to get load info: ${error.message}`);
1223
+ } else {
1224
+ output.printError(`Unexpected error: ${String(error)}`);
1225
+ }
1226
+ return { success: false, exitCode: 1 };
1227
+ }
1228
+ }
1229
+ };
1230
+
1231
+ const rebalanceCommand: Command = {
1232
+ name: 'rebalance',
1233
+ description: 'Trigger swarm rebalancing',
1234
+ options: [
1235
+ {
1236
+ name: 'dry-run',
1237
+ short: 'd',
1238
+ description: 'Preview rebalancing without making changes',
1239
+ type: 'boolean',
1240
+ default: false
1241
+ },
1242
+ {
1243
+ name: 'force',
1244
+ short: 'f',
1245
+ description: 'Force rebalancing without confirmation',
1246
+ type: 'boolean',
1247
+ default: false
1248
+ },
1249
+ {
1250
+ name: 'threshold',
1251
+ short: 't',
1252
+ description: 'Utilization threshold for rebalancing (0-100)',
1253
+ type: 'number',
1254
+ default: 80
1255
+ }
1256
+ ],
1257
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
1258
+ const dryRun = ctx.flags['dry-run'] as boolean;
1259
+ const force = ctx.flags.force as boolean;
1260
+ const threshold = ctx.flags.threshold as number;
1261
+
1262
+ if (!dryRun && !force && ctx.interactive) {
1263
+ output.writeln();
1264
+ output.printWarning('This will reassign issues between agents to balance load.');
1265
+
1266
+ const confirmed = await confirm({
1267
+ message: 'Proceed with rebalancing?',
1268
+ default: false
1269
+ });
1270
+
1271
+ if (!confirmed) {
1272
+ output.printInfo('Operation cancelled');
1273
+ return { success: true };
1274
+ }
1275
+ }
1276
+
1277
+ output.writeln();
1278
+
1279
+ if (dryRun) {
1280
+ output.printInfo('Analyzing rebalancing options (dry run)...');
1281
+ } else {
1282
+ output.printInfo('Rebalancing swarm workload...');
1283
+ }
1284
+
1285
+ const spinner = output.createSpinner({ text: 'Calculating optimal distribution...', spinner: 'dots' });
1286
+ spinner.start();
1287
+
1288
+ try {
1289
+ const result = await callMCPTool<RebalanceResult>('claims/rebalance', {
1290
+ dryRun,
1291
+ threshold
1292
+ });
1293
+
1294
+ spinner.stop();
1295
+
1296
+ output.writeln();
1297
+
1298
+ if (dryRun) {
1299
+ output.printSuccess('Rebalancing Analysis Complete (Dry Run)');
1300
+ } else {
1301
+ output.printSuccess('Rebalancing Complete');
1302
+ }
1303
+
1304
+ output.writeln();
1305
+
1306
+ // Summary stats
1307
+ output.printList([
1308
+ `Issues to move: ${output.highlight(String(result.moved))}`,
1309
+ `Issues skipped: ${output.dim(String(result.skipped))}`,
1310
+ `Mode: ${dryRun ? output.warning('DRY RUN') : output.success('APPLIED')}`
1311
+ ]);
1312
+
1313
+ if (result.reassignments.length > 0) {
1314
+ output.writeln();
1315
+ output.writeln(output.bold('Reassignments'));
1316
+
1317
+ output.printTable({
1318
+ columns: [
1319
+ { key: 'issue', header: 'Issue', width: 12 },
1320
+ { key: 'from', header: 'From', width: 15 },
1321
+ { key: 'to', header: 'To', width: 15 },
1322
+ { key: 'reason', header: 'Reason', width: 30 }
1323
+ ],
1324
+ data: result.reassignments.map(r => ({
1325
+ issue: r.issueId,
1326
+ from: r.fromAgent,
1327
+ to: r.toAgent,
1328
+ reason: r.reason
1329
+ }))
1330
+ });
1331
+
1332
+ if (dryRun) {
1333
+ output.writeln();
1334
+ output.printInfo('Run without --dry-run to apply these changes');
1335
+ }
1336
+ } else {
1337
+ output.writeln();
1338
+ output.printInfo('No reassignments needed - workload is balanced');
1339
+ }
1340
+
1341
+ if (ctx.flags.format === 'json') {
1342
+ output.printJson(result);
1343
+ }
1344
+
1345
+ return { success: true, data: result };
1346
+ } catch (error) {
1347
+ spinner.fail('Rebalancing failed');
1348
+
1349
+ if (error instanceof MCPClientError) {
1350
+ output.printError(`Failed to rebalance: ${error.message}`);
1351
+ } else {
1352
+ output.printError(`Unexpected error: ${String(error)}`);
1353
+ }
1354
+ return { success: false, exitCode: 1 };
1355
+ }
1356
+ }
1357
+ };
1358
+
1359
+ // ============================================
1360
+ // Main Issues Command
1361
+ // ============================================
1362
+
1363
+ export const issuesCommand: Command = {
1364
+ name: 'issues',
1365
+ description: 'Manage issue claims and work distribution',
1366
+ subcommands: [
1367
+ // Core claiming
1368
+ listCommand,
1369
+ claimCommand,
1370
+ releaseCommand,
1371
+ handoffCommand,
1372
+ statusCommand,
1373
+ boardCommand,
1374
+ // Work stealing
1375
+ stealableCommand,
1376
+ stealCommand,
1377
+ markStealableCommand,
1378
+ contestCommand,
1379
+ // Load balancing
1380
+ loadCommand,
1381
+ rebalanceCommand
1382
+ ],
1383
+ options: [],
1384
+ examples: [
1385
+ { command: '@sparkleideas/claude-flow issues list --available', description: 'List unclaimed issues' },
1386
+ { command: '@sparkleideas/claude-flow issues list --mine', description: 'List my claims' },
1387
+ { command: '@sparkleideas/claude-flow issues claim GH-123', description: 'Claim an issue' },
1388
+ { command: '@sparkleideas/claude-flow issues release GH-123', description: 'Release a claim' },
1389
+ { command: '@sparkleideas/claude-flow issues handoff GH-123 --to agent:coder-1', description: 'Request handoff to agent' },
1390
+ { command: '@sparkleideas/claude-flow issues handoff GH-123 --to human:alice', description: 'Request handoff to human' },
1391
+ { command: '@sparkleideas/claude-flow issues status GH-123 --blocked "Waiting for API"', description: 'Mark as blocked' },
1392
+ { command: '@sparkleideas/claude-flow issues status GH-123 --review-requested', description: 'Request review' },
1393
+ { command: '@sparkleideas/claude-flow issues board', description: 'View who is working on what' },
1394
+ { command: '@sparkleideas/claude-flow issues stealable', description: 'List stealable issues' },
1395
+ { command: '@sparkleideas/claude-flow issues steal GH-123', description: 'Steal an issue' },
1396
+ { command: '@sparkleideas/claude-flow issues mark-stealable GH-123', description: 'Mark my claim as stealable' },
1397
+ { command: '@sparkleideas/claude-flow issues contest GH-123 -r "I was actively working on it"', description: 'Contest a steal' },
1398
+ { command: '@sparkleideas/claude-flow issues load', description: 'View agent load distribution' },
1399
+ { command: '@sparkleideas/claude-flow issues load --agent coder-1', description: 'View specific agent load' },
1400
+ { command: '@sparkleideas/claude-flow issues rebalance --dry-run', description: 'Preview rebalancing' },
1401
+ { command: '@sparkleideas/claude-flow issues rebalance', description: 'Trigger swarm rebalancing' }
1402
+ ],
1403
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
1404
+ // Show help if no subcommand
1405
+ output.writeln();
1406
+ output.writeln(output.bold('Issue Claims Management'));
1407
+ output.writeln();
1408
+ output.writeln('Usage: @sparkleideas/claude-flow issues <subcommand> [options]');
1409
+ output.writeln();
1410
+
1411
+ output.writeln(output.bold('Core Commands'));
1412
+ output.printList([
1413
+ `${output.highlight('list')} - List issues (--available, --mine)`,
1414
+ `${output.highlight('claim')} - Claim an issue to work on`,
1415
+ `${output.highlight('release')} - Release a claim on an issue`,
1416
+ `${output.highlight('handoff')} - Request handoff to another agent/human`,
1417
+ `${output.highlight('status')} - Update or view issue claim status`,
1418
+ `${output.highlight('board')} - View who is working on what`
1419
+ ]);
1420
+
1421
+ output.writeln();
1422
+ output.writeln(output.bold('Work Stealing Commands'));
1423
+ output.printList([
1424
+ `${output.highlight('stealable')} - List stealable issues`,
1425
+ `${output.highlight('steal')} - Steal an issue from another agent`,
1426
+ `${output.highlight('mark-stealable')} - Mark my claim as stealable`,
1427
+ `${output.highlight('contest')} - Contest a steal action`
1428
+ ]);
1429
+
1430
+ output.writeln();
1431
+ output.writeln(output.bold('Load Balancing Commands'));
1432
+ output.printList([
1433
+ `${output.highlight('load')} - View agent load distribution`,
1434
+ `${output.highlight('rebalance')} - Trigger swarm rebalancing`
1435
+ ]);
1436
+
1437
+ output.writeln();
1438
+ output.writeln('Run "@sparkleideas/claude-flow issues <subcommand> --help" for subcommand help');
1439
+
1440
+ return { success: true };
1441
+ }
1442
+ };
1443
+
1444
+ // ============================================
1445
+ // Factory Function (for dependency injection)
1446
+ // ============================================
1447
+
1448
+ /**
1449
+ * Create issues command with injected services
1450
+ * This allows for testing with mock services
1451
+ */
1452
+ export function createIssuesCommand(services: ClaimServices): Command {
1453
+ // The command structure remains the same, but actions would use
1454
+ // the injected services instead of callMCPTool
1455
+ // For now, we return the default command which uses MCP tools
1456
+ return issuesCommand;
1457
+ }
1458
+
1459
+ export default issuesCommand;