@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.
- package/README.md +261 -0
- package/package.json +84 -0
- package/src/api/cli-commands.ts +1459 -0
- package/src/api/cli-types.ts +154 -0
- package/src/api/index.ts +24 -0
- package/src/api/mcp-tools.ts +1977 -0
- package/src/application/claim-service.ts +753 -0
- package/src/application/index.ts +46 -0
- package/src/application/load-balancer.ts +840 -0
- package/src/application/work-stealing-service.ts +807 -0
- package/src/domain/events.ts +779 -0
- package/src/domain/index.ts +214 -0
- package/src/domain/repositories.ts +239 -0
- package/src/domain/rules.ts +526 -0
- package/src/domain/types.ts +826 -0
- package/src/index.ts +79 -0
- package/src/infrastructure/claim-repository.ts +358 -0
- package/src/infrastructure/event-store.ts +297 -0
- package/src/infrastructure/index.ts +21 -0
|
@@ -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;
|