@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/claims (ADR-016)
|
|
3
|
+
*
|
|
4
|
+
* Issue claiming and handoff management for human and agent collaboration.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Issue claiming and releasing
|
|
8
|
+
* - Human-to-agent and agent-to-agent handoffs
|
|
9
|
+
* - Status tracking and updates (active, paused, handoff-pending, review-requested, blocked, stealable, completed)
|
|
10
|
+
* - Auto-management (expiration, auto-assignment)
|
|
11
|
+
* - Work stealing with contest windows
|
|
12
|
+
* - Load balancing and swarm rebalancing
|
|
13
|
+
* - Full event sourcing (ADR-007)
|
|
14
|
+
*
|
|
15
|
+
* MCP Tools (17 total):
|
|
16
|
+
* - Core Claiming (7): claim, release, handoff, status_update, list_available, list_mine, board
|
|
17
|
+
* - Work Stealing (4): mark_stealable, steal, get_stealable, contest_steal
|
|
18
|
+
* - Load Balancing (3): agent_load_info, swarm_rebalance, swarm_load_overview
|
|
19
|
+
* - Additional (3): claim_history, claim_metrics, claim_config
|
|
20
|
+
*
|
|
21
|
+
* ADR-016 Types:
|
|
22
|
+
* - ClaimStatus: active | paused | handoff-pending | review-requested | blocked | stealable | completed
|
|
23
|
+
* - ClaimantType: human | agent
|
|
24
|
+
* - StealReason: timeout | overloaded | blocked | voluntary | rebalancing | abandoned | priority-change
|
|
25
|
+
* - HandoffReason: capacity | expertise | shift-change | escalation | voluntary | rebalancing
|
|
26
|
+
*
|
|
27
|
+
* @module v3/claims
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Domain layer - Types, Events, Rules, Repositories
|
|
31
|
+
export * from './domain/index.js';
|
|
32
|
+
|
|
33
|
+
// Application layer - Services
|
|
34
|
+
export * from './application/index.js';
|
|
35
|
+
|
|
36
|
+
// Infrastructure layer - Persistence
|
|
37
|
+
export * from './infrastructure/index.js';
|
|
38
|
+
|
|
39
|
+
// API layer - MCP Tools
|
|
40
|
+
export {
|
|
41
|
+
// All tools collection
|
|
42
|
+
claimsTools,
|
|
43
|
+
|
|
44
|
+
// Tool categories
|
|
45
|
+
coreClaimingTools,
|
|
46
|
+
workStealingTools,
|
|
47
|
+
loadBalancingTools,
|
|
48
|
+
additionalClaimsTools,
|
|
49
|
+
|
|
50
|
+
// Core Claiming Tools (7)
|
|
51
|
+
issueClaimTool,
|
|
52
|
+
issueReleaseTool,
|
|
53
|
+
issueHandoffTool,
|
|
54
|
+
issueStatusUpdateTool,
|
|
55
|
+
issueListAvailableTool,
|
|
56
|
+
issueListMineTool,
|
|
57
|
+
issueBoardTool,
|
|
58
|
+
|
|
59
|
+
// Work Stealing Tools (4)
|
|
60
|
+
issueMarkStealableTool,
|
|
61
|
+
issueStealTool,
|
|
62
|
+
issueGetStealableTool,
|
|
63
|
+
issueContestStealTool,
|
|
64
|
+
|
|
65
|
+
// Load Balancing Tools (3)
|
|
66
|
+
agentLoadInfoTool,
|
|
67
|
+
swarmRebalanceTool,
|
|
68
|
+
swarmLoadOverviewTool,
|
|
69
|
+
|
|
70
|
+
// Additional Tools (3)
|
|
71
|
+
claimHistoryTool,
|
|
72
|
+
claimMetricsTool,
|
|
73
|
+
claimConfigTool,
|
|
74
|
+
|
|
75
|
+
// Utility functions
|
|
76
|
+
registerClaimsTools,
|
|
77
|
+
getClaimsToolsByCategory,
|
|
78
|
+
getClaimsToolByName,
|
|
79
|
+
} from './api/mcp-tools.js';
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/claims - Claim Repository Implementation
|
|
3
|
+
* SQLite-based persistence for claims (ADR-016)
|
|
4
|
+
*
|
|
5
|
+
* @module v3/claims/infrastructure/claim-repository
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ClaimId,
|
|
10
|
+
IssueId,
|
|
11
|
+
Claimant,
|
|
12
|
+
ClaimStatus,
|
|
13
|
+
IssueClaim,
|
|
14
|
+
IssueClaimWithStealing,
|
|
15
|
+
AgentType,
|
|
16
|
+
ExtendedClaimStatus,
|
|
17
|
+
ExtendedIssueClaim,
|
|
18
|
+
ClaimQueryOptions,
|
|
19
|
+
ClaimStatistics,
|
|
20
|
+
} from '../domain/types.js';
|
|
21
|
+
import { IClaimRepository } from '../domain/repositories.js';
|
|
22
|
+
import { IIssueClaimRepository } from '../domain/types.js';
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// In-Memory Claim Repository (Default Implementation)
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* In-memory implementation of the claim repository
|
|
30
|
+
* Suitable for development and testing
|
|
31
|
+
*/
|
|
32
|
+
export class InMemoryClaimRepository implements IClaimRepository, IIssueClaimRepository {
|
|
33
|
+
private claims: Map<ClaimId, IssueClaimWithStealing> = new Map();
|
|
34
|
+
private issueIndex: Map<string, ClaimId> = new Map(); // issueId:repo -> claimId
|
|
35
|
+
private claimantIndex: Map<string, Set<ClaimId>> = new Map(); // claimantId -> claimIds
|
|
36
|
+
|
|
37
|
+
async initialize(): Promise<void> {
|
|
38
|
+
// No initialization needed for in-memory store
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async shutdown(): Promise<void> {
|
|
42
|
+
this.claims.clear();
|
|
43
|
+
this.issueIndex.clear();
|
|
44
|
+
this.claimantIndex.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ==========================================================================
|
|
48
|
+
// CRUD Operations
|
|
49
|
+
// ==========================================================================
|
|
50
|
+
|
|
51
|
+
async save(claim: IssueClaim | IssueClaimWithStealing): Promise<void> {
|
|
52
|
+
const fullClaim = this.ensureFullClaim(claim);
|
|
53
|
+
this.claims.set(claim.id, fullClaim);
|
|
54
|
+
|
|
55
|
+
// Update indexes
|
|
56
|
+
const issueKey = this.getIssueKey(claim.issueId, (claim as any).repository ?? '');
|
|
57
|
+
this.issueIndex.set(issueKey, claim.id);
|
|
58
|
+
|
|
59
|
+
const claimantId = claim.claimant.id;
|
|
60
|
+
if (!this.claimantIndex.has(claimantId)) {
|
|
61
|
+
this.claimantIndex.set(claimantId, new Set());
|
|
62
|
+
}
|
|
63
|
+
this.claimantIndex.get(claimantId)!.add(claim.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async update(claim: IssueClaimWithStealing): Promise<void> {
|
|
67
|
+
await this.save(claim);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async findById(claimId: ClaimId): Promise<IssueClaimWithStealing | null> {
|
|
71
|
+
return this.claims.get(claimId) ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async findByIssueId(issueId: IssueId, repository?: string): Promise<IssueClaimWithStealing | null> {
|
|
75
|
+
const issueKey = this.getIssueKey(issueId, repository ?? '');
|
|
76
|
+
const claimId = this.issueIndex.get(issueKey);
|
|
77
|
+
if (!claimId) return null;
|
|
78
|
+
|
|
79
|
+
const claim = this.claims.get(claimId);
|
|
80
|
+
if (!claim) return null;
|
|
81
|
+
|
|
82
|
+
// Return only if claim is active
|
|
83
|
+
if (this.isActiveStatus(claim.status)) {
|
|
84
|
+
return claim;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async findByClaimant(claimant: Claimant): Promise<IssueClaim[]> {
|
|
90
|
+
return this.findByAgentId(claimant.id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async findByAgentId(agentId: string): Promise<IssueClaimWithStealing[]> {
|
|
94
|
+
const claimIds = this.claimantIndex.get(agentId);
|
|
95
|
+
if (!claimIds) return [];
|
|
96
|
+
|
|
97
|
+
return Array.from(claimIds)
|
|
98
|
+
.map((id) => this.claims.get(id))
|
|
99
|
+
.filter((c): c is IssueClaimWithStealing => c !== undefined);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async findByStatus(status: ClaimStatus): Promise<IssueClaim[]> {
|
|
103
|
+
return Array.from(this.claims.values()).filter((c) => c.status === status);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async findStealable(agentType?: AgentType): Promise<IssueClaimWithStealing[]> {
|
|
107
|
+
return Array.from(this.claims.values()).filter((c) => {
|
|
108
|
+
if (c.status !== 'stealable') return false;
|
|
109
|
+
if (!c.stealInfo) return true;
|
|
110
|
+
if (!agentType) return true;
|
|
111
|
+
if (!c.stealInfo.allowedStealerTypes) return true;
|
|
112
|
+
return c.stealInfo.allowedStealerTypes.includes(agentType);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async findContested(): Promise<IssueClaimWithStealing[]> {
|
|
117
|
+
return Array.from(this.claims.values()).filter(
|
|
118
|
+
(c) => c.contestInfo && !c.contestInfo.resolution
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async findAll(): Promise<IssueClaimWithStealing[]> {
|
|
123
|
+
return Array.from(this.claims.values());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async delete(claimId: ClaimId): Promise<void> {
|
|
127
|
+
const claim = this.claims.get(claimId);
|
|
128
|
+
if (claim) {
|
|
129
|
+
// Remove from indexes
|
|
130
|
+
const issueKey = this.getIssueKey(claim.issueId, (claim as any).repository ?? '');
|
|
131
|
+
this.issueIndex.delete(issueKey);
|
|
132
|
+
|
|
133
|
+
const claimantId = claim.claimant.id;
|
|
134
|
+
const claimantClaims = this.claimantIndex.get(claimantId);
|
|
135
|
+
if (claimantClaims) {
|
|
136
|
+
claimantClaims.delete(claimId);
|
|
137
|
+
if (claimantClaims.size === 0) {
|
|
138
|
+
this.claimantIndex.delete(claimantId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.claims.delete(claimId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ==========================================================================
|
|
147
|
+
// Query Operations
|
|
148
|
+
// ==========================================================================
|
|
149
|
+
|
|
150
|
+
async findActiveClaims(): Promise<IssueClaim[]> {
|
|
151
|
+
return Array.from(this.claims.values()).filter((c) =>
|
|
152
|
+
this.isActiveStatus(c.status)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async findStaleClaims(staleSince: Date): Promise<IssueClaim[]> {
|
|
157
|
+
const staleTimestamp = staleSince.getTime();
|
|
158
|
+
return Array.from(this.claims.values()).filter(
|
|
159
|
+
(c) =>
|
|
160
|
+
this.isActiveStatus(c.status) &&
|
|
161
|
+
c.lastActivityAt.getTime() < staleTimestamp
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async findClaimsWithPendingHandoffs(): Promise<IssueClaim[]> {
|
|
166
|
+
return Array.from(this.claims.values()).filter(
|
|
167
|
+
(c) => c.status === 'pending_handoff'
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async countByClaimant(claimantId: string): Promise<number> {
|
|
172
|
+
return this.claimantIndex.get(claimantId)?.size ?? 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async countByAgentId(agentId: string): Promise<number> {
|
|
176
|
+
const claims = await this.findByAgentId(agentId);
|
|
177
|
+
return claims.filter((c) => this.isActiveStatus(c.status)).length;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
// Extended Query Operations (ADR-016)
|
|
182
|
+
// ==========================================================================
|
|
183
|
+
|
|
184
|
+
async query(options: ClaimQueryOptions): Promise<IssueClaimWithStealing[]> {
|
|
185
|
+
let results = Array.from(this.claims.values());
|
|
186
|
+
|
|
187
|
+
// Apply filters
|
|
188
|
+
if (options.claimantId) {
|
|
189
|
+
results = results.filter((c) => c.claimant.id === options.claimantId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.claimantType) {
|
|
193
|
+
results = results.filter((c) => c.claimant.type === options.claimantType);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (options.status) {
|
|
197
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
198
|
+
results = results.filter((c) => statuses.includes(c.status as any));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options.repository) {
|
|
202
|
+
results = results.filter((c) => (c as any).repository === options.repository);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (options.issueId) {
|
|
206
|
+
results = results.filter((c) => c.issueId === options.issueId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (options.stealableOnly) {
|
|
210
|
+
results = results.filter((c) => c.status === 'stealable');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.blockedOnly) {
|
|
214
|
+
results = results.filter((c) => c.blockedReason !== undefined);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.createdAfter) {
|
|
218
|
+
results = results.filter((c) => c.claimedAt.getTime() >= options.createdAfter!);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (options.createdBefore) {
|
|
222
|
+
results = results.filter((c) => c.claimedAt.getTime() <= options.createdBefore!);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (options.updatedAfter) {
|
|
226
|
+
results = results.filter((c) => c.lastActivityAt.getTime() >= options.updatedAfter!);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Apply sorting
|
|
230
|
+
if (options.sortBy) {
|
|
231
|
+
results.sort((a, b) => {
|
|
232
|
+
let aVal: number, bVal: number;
|
|
233
|
+
|
|
234
|
+
switch (options.sortBy) {
|
|
235
|
+
case 'claimedAt':
|
|
236
|
+
aVal = a.claimedAt.getTime();
|
|
237
|
+
bVal = b.claimedAt.getTime();
|
|
238
|
+
break;
|
|
239
|
+
case 'updatedAt':
|
|
240
|
+
aVal = a.lastActivityAt.getTime();
|
|
241
|
+
bVal = b.lastActivityAt.getTime();
|
|
242
|
+
break;
|
|
243
|
+
case 'progress':
|
|
244
|
+
aVal = a.progress;
|
|
245
|
+
bVal = b.progress;
|
|
246
|
+
break;
|
|
247
|
+
default:
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return options.sortDirection === 'desc' ? bVal - aVal : aVal - bVal;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Apply pagination
|
|
256
|
+
if (options.offset) {
|
|
257
|
+
results = results.slice(options.offset);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (options.limit) {
|
|
261
|
+
results = results.slice(0, options.limit);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return results;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async getStatistics(): Promise<ClaimStatistics> {
|
|
268
|
+
const claims = Array.from(this.claims.values());
|
|
269
|
+
|
|
270
|
+
const byStatus: Record<string, number> = {
|
|
271
|
+
active: 0,
|
|
272
|
+
paused: 0,
|
|
273
|
+
'handoff-pending': 0,
|
|
274
|
+
'review-requested': 0,
|
|
275
|
+
blocked: 0,
|
|
276
|
+
stealable: 0,
|
|
277
|
+
completed: 0,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const byClaimantType: Record<string, number> = {
|
|
281
|
+
human: 0,
|
|
282
|
+
agent: 0,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const byRepository: Record<string, number> = {};
|
|
286
|
+
let totalDuration = 0;
|
|
287
|
+
let completedCount = 0;
|
|
288
|
+
let totalProgress = 0;
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
const last24h = now - 24 * 60 * 60 * 1000;
|
|
291
|
+
|
|
292
|
+
for (const claim of claims) {
|
|
293
|
+
byStatus[claim.status] = (byStatus[claim.status] ?? 0) + 1;
|
|
294
|
+
byClaimantType[claim.claimant.type] = (byClaimantType[claim.claimant.type] ?? 0) + 1;
|
|
295
|
+
|
|
296
|
+
const repo = (claim as any).repository ?? 'unknown';
|
|
297
|
+
byRepository[repo] = (byRepository[repo] ?? 0) + 1;
|
|
298
|
+
|
|
299
|
+
totalProgress += claim.progress;
|
|
300
|
+
|
|
301
|
+
if (claim.status === 'completed') {
|
|
302
|
+
completedCount++;
|
|
303
|
+
const duration = claim.lastActivityAt.getTime() - claim.claimedAt.getTime();
|
|
304
|
+
totalDuration += duration;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const completedLast24h = claims.filter(
|
|
309
|
+
(c) => c.status === 'completed' && c.lastActivityAt.getTime() >= last24h
|
|
310
|
+
).length;
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
totalClaims: claims.length,
|
|
314
|
+
byStatus: byStatus as any,
|
|
315
|
+
byPriority: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
316
|
+
byClaimantType: byClaimantType as any,
|
|
317
|
+
avgDurationMs: completedCount > 0 ? totalDuration / completedCount : 0,
|
|
318
|
+
avgProgress: claims.length > 0 ? totalProgress / claims.length : 0,
|
|
319
|
+
activeSteals: claims.filter((c) => c.status === 'stealable').length,
|
|
320
|
+
pendingHandoffs: claims.filter((c) => c.status === 'pending_handoff').length,
|
|
321
|
+
completedLast24h,
|
|
322
|
+
byRepository,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ==========================================================================
|
|
327
|
+
// Helper Methods
|
|
328
|
+
// ==========================================================================
|
|
329
|
+
|
|
330
|
+
private getIssueKey(issueId: IssueId, repository: string): string {
|
|
331
|
+
return `${repository}:${issueId}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private isActiveStatus(status: string): boolean {
|
|
335
|
+
return ['active', 'paused', 'blocked', 'pending_handoff', 'in_review', 'stealable'].includes(
|
|
336
|
+
status
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private ensureFullClaim(claim: IssueClaim | IssueClaimWithStealing): IssueClaimWithStealing {
|
|
341
|
+
const fullClaim = claim as IssueClaimWithStealing;
|
|
342
|
+
if (fullClaim.progress === undefined) {
|
|
343
|
+
(fullClaim as any).progress = 0;
|
|
344
|
+
}
|
|
345
|
+
return fullClaim;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// =============================================================================
|
|
350
|
+
// Factory Function
|
|
351
|
+
// =============================================================================
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create a new claim repository
|
|
355
|
+
*/
|
|
356
|
+
export function createClaimRepository(): InMemoryClaimRepository {
|
|
357
|
+
return new InMemoryClaimRepository();
|
|
358
|
+
}
|