@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,826 @@
1
+ /**
2
+ * Claims Domain Types
3
+ *
4
+ * Core type definitions for the issue claiming system.
5
+ * Supports both human and agent claimants with handoff capabilities.
6
+ *
7
+ * @module v3/claims/domain/types
8
+ */
9
+
10
+ // =============================================================================
11
+ // Core Types
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Unique identifier for claims
16
+ */
17
+ export type ClaimId = `claim-${string}`;
18
+
19
+ /**
20
+ * Unique identifier for issues
21
+ */
22
+ export type IssueId = string;
23
+
24
+ /**
25
+ * Claimant type - human or agent
26
+ */
27
+ export type ClaimantType = 'human' | 'agent';
28
+
29
+ /**
30
+ * Claim status lifecycle
31
+ */
32
+ export type ClaimStatus =
33
+ | 'active' // Claim is active and work is in progress
34
+ | 'pending_handoff' // Handoff has been requested but not accepted
35
+ | 'in_review' // Work is complete, awaiting review
36
+ | 'completed' // Claim is completed successfully
37
+ | 'released' // Claim was released voluntarily
38
+ | 'expired' // Claim expired due to inactivity
39
+ | 'paused' // Work is temporarily paused
40
+ | 'blocked' // Work is blocked by external dependency
41
+ | 'stealable'; // Claim can be stolen by another agent
42
+
43
+ /**
44
+ * Issue labels/tags
45
+ */
46
+ export type IssueLabel = string;
47
+
48
+ /**
49
+ * Issue priority levels
50
+ */
51
+ export type IssuePriority = 'critical' | 'high' | 'medium' | 'low';
52
+
53
+ /**
54
+ * Issue complexity levels
55
+ */
56
+ export type IssueComplexity = 'trivial' | 'simple' | 'moderate' | 'complex' | 'epic';
57
+
58
+ // =============================================================================
59
+ // Value Objects
60
+ // =============================================================================
61
+
62
+ /**
63
+ * Duration value object for time-based operations
64
+ */
65
+ export interface Duration {
66
+ value: number;
67
+ unit: 'ms' | 'seconds' | 'minutes' | 'hours' | 'days';
68
+ }
69
+
70
+ /**
71
+ * Convert duration to milliseconds
72
+ */
73
+ export function durationToMs(duration: Duration): number {
74
+ const multipliers: Record<Duration['unit'], number> = {
75
+ ms: 1,
76
+ seconds: 1000,
77
+ minutes: 60 * 1000,
78
+ hours: 60 * 60 * 1000,
79
+ days: 24 * 60 * 60 * 1000,
80
+ };
81
+ return duration.value * multipliers[duration.unit];
82
+ }
83
+
84
+ // =============================================================================
85
+ // Entity Interfaces
86
+ // =============================================================================
87
+
88
+ /**
89
+ * Claimant - a human or agent that can claim issues
90
+ */
91
+ export interface Claimant {
92
+ id: string;
93
+ type: ClaimantType;
94
+ name: string;
95
+ capabilities?: string[];
96
+ specializations?: string[];
97
+ currentWorkload?: number;
98
+ maxConcurrentClaims?: number;
99
+ metadata?: Record<string, unknown>;
100
+ }
101
+
102
+ /**
103
+ * Issue - a work item that can be claimed
104
+ */
105
+ export interface Issue {
106
+ id: IssueId;
107
+ title: string;
108
+ description: string;
109
+ labels: IssueLabel[];
110
+ priority: IssuePriority;
111
+ complexity: IssueComplexity;
112
+ requiredCapabilities?: string[];
113
+ estimatedDuration?: Duration;
114
+ repositoryId?: string;
115
+ url?: string;
116
+ createdAt: Date;
117
+ updatedAt: Date;
118
+ metadata?: Record<string, unknown>;
119
+ }
120
+
121
+ /**
122
+ * Issue claim - represents an active claim on an issue
123
+ */
124
+ export interface IssueClaim {
125
+ id: ClaimId;
126
+ issueId: IssueId;
127
+ claimant: Claimant;
128
+ status: ClaimStatus;
129
+ claimedAt: Date;
130
+ lastActivityAt: Date;
131
+ expiresAt?: Date;
132
+ notes?: string[];
133
+ handoffChain?: HandoffRecord[];
134
+ reviewers?: Claimant[];
135
+ metadata?: Record<string, unknown>;
136
+ }
137
+
138
+ /**
139
+ * Handoff record - tracks handoff history
140
+ */
141
+ export interface HandoffRecord {
142
+ id: string;
143
+ from: Claimant;
144
+ to: Claimant;
145
+ reason: string;
146
+ status: 'pending' | 'accepted' | 'rejected';
147
+ requestedAt: Date;
148
+ resolvedAt?: Date;
149
+ rejectionReason?: string;
150
+ }
151
+
152
+ /**
153
+ * Issue with claim information
154
+ */
155
+ export interface IssueWithClaim {
156
+ issue: Issue;
157
+ claim: IssueClaim | null;
158
+ pendingHandoffs: HandoffRecord[];
159
+ }
160
+
161
+ /**
162
+ * Claim result - returned when claiming an issue
163
+ */
164
+ export interface ClaimResult {
165
+ success: boolean;
166
+ claim?: IssueClaim;
167
+ error?: ClaimError;
168
+ }
169
+
170
+ // =============================================================================
171
+ // Filter/Query Types
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Filters for querying issues
176
+ */
177
+ export interface IssueFilters {
178
+ labels?: IssueLabel[];
179
+ priority?: IssuePriority[];
180
+ complexity?: IssueComplexity[];
181
+ requiredCapabilities?: string[];
182
+ excludeClaimed?: boolean;
183
+ repositoryId?: string;
184
+ limit?: number;
185
+ offset?: number;
186
+ }
187
+
188
+ // =============================================================================
189
+ // Error Types
190
+ // =============================================================================
191
+
192
+ /**
193
+ * Claim error codes
194
+ */
195
+ export type ClaimErrorCode =
196
+ | 'ALREADY_CLAIMED'
197
+ | 'NOT_CLAIMED'
198
+ | 'CLAIM_NOT_FOUND'
199
+ | 'ISSUE_NOT_FOUND'
200
+ | 'CLAIMANT_NOT_FOUND'
201
+ | 'INVALID_CLAIMANT'
202
+ | 'HANDOFF_PENDING'
203
+ | 'HANDOFF_NOT_FOUND'
204
+ | 'UNAUTHORIZED'
205
+ | 'MAX_CLAIMS_EXCEEDED'
206
+ | 'CAPABILITY_MISMATCH'
207
+ | 'INVALID_STATUS_TRANSITION'
208
+ | 'VALIDATION_ERROR';
209
+
210
+ /**
211
+ * Claim error with details
212
+ */
213
+ export interface ClaimError {
214
+ code: ClaimErrorCode;
215
+ message: string;
216
+ details?: Record<string, unknown>;
217
+ }
218
+
219
+ /**
220
+ * Custom error class for claim operations
221
+ */
222
+ export class ClaimOperationError extends Error {
223
+ constructor(
224
+ public readonly code: ClaimErrorCode,
225
+ message: string,
226
+ public readonly details?: Record<string, unknown>
227
+ ) {
228
+ super(message);
229
+ this.name = 'ClaimOperationError';
230
+ }
231
+
232
+ toClaimError(): ClaimError {
233
+ return {
234
+ code: this.code,
235
+ message: this.message,
236
+ details: this.details,
237
+ };
238
+ }
239
+ }
240
+
241
+ // =============================================================================
242
+ // Work Stealing Types
243
+ // =============================================================================
244
+
245
+ /**
246
+ * Agent type for cross-type steal rules
247
+ */
248
+ export type AgentType =
249
+ | 'coder'
250
+ | 'debugger'
251
+ | 'tester'
252
+ | 'reviewer'
253
+ | 'researcher'
254
+ | 'planner'
255
+ | 'architect'
256
+ | 'coordinator';
257
+
258
+ /**
259
+ * Reason an issue became stealable
260
+ */
261
+ export type StealableReason =
262
+ | 'stale' // No progress for staleThresholdMinutes
263
+ | 'blocked' // Blocked for blockedThresholdMinutes
264
+ | 'overloaded' // Agent has too many claims
265
+ | 'manual' // Manually marked as stealable
266
+ | 'timeout'; // Task timed out
267
+
268
+ /**
269
+ * Information about why an issue is stealable
270
+ */
271
+ export interface StealableInfo {
272
+ reason: StealableReason;
273
+ markedAt: Date;
274
+ expiresAt?: Date;
275
+ originalProgress: number;
276
+ allowedStealerTypes?: AgentType[];
277
+ }
278
+
279
+ /**
280
+ * Error codes specific to steal operations
281
+ */
282
+ export type StealErrorCode =
283
+ | 'NOT_STEALABLE'
284
+ | 'ALREADY_CLAIMED'
285
+ | 'CROSS_TYPE_NOT_ALLOWED'
286
+ | 'IN_GRACE_PERIOD'
287
+ | 'PROTECTED_BY_PROGRESS'
288
+ | 'STEALER_OVERLOADED'
289
+ | 'ISSUE_NOT_FOUND'
290
+ | 'CONTEST_PENDING';
291
+
292
+ /**
293
+ * Result of a steal operation
294
+ */
295
+ export interface StealResult {
296
+ success: boolean;
297
+ claim?: IssueClaim;
298
+ previousClaimant?: Claimant;
299
+ contestWindowEndsAt?: Date;
300
+ error?: string;
301
+ errorCode?: StealErrorCode;
302
+ }
303
+
304
+ /**
305
+ * Information about a contested steal
306
+ */
307
+ export interface ContestInfo {
308
+ contestedAt: Date;
309
+ contestedBy: Claimant;
310
+ stolenBy: Claimant;
311
+ reason: string;
312
+ windowEndsAt: Date;
313
+ resolution?: ContestResolution;
314
+ }
315
+
316
+ /**
317
+ * Resolution of a contest
318
+ */
319
+ export interface ContestResolution {
320
+ resolvedAt: Date;
321
+ winner: Claimant;
322
+ resolvedBy: 'queen' | 'human' | 'timeout';
323
+ reason: string;
324
+ }
325
+
326
+ /**
327
+ * Work stealing configuration
328
+ */
329
+ export interface WorkStealingConfig {
330
+ /** Minutes without progress before issue becomes stealable */
331
+ staleThresholdMinutes: number;
332
+
333
+ /** Minutes blocked before issue becomes stealable */
334
+ blockedThresholdMinutes: number;
335
+
336
+ /** Max claims per agent before lowest priority becomes stealable */
337
+ overloadThreshold: number;
338
+
339
+ /** Minutes after claiming before work can be stolen */
340
+ gracePeriodMinutes: number;
341
+
342
+ /** Progress percentage that protects from stealing (0-100) */
343
+ minProgressToProtect: number;
344
+
345
+ /** Minutes to contest a steal */
346
+ contestWindowMinutes: number;
347
+
348
+ /** Enable cross-type stealing */
349
+ allowCrossTypeSteal: boolean;
350
+
351
+ /** Agent type pairs that can steal from each other */
352
+ crossTypeStealRules: [AgentType, AgentType][];
353
+ }
354
+
355
+ /**
356
+ * Default work stealing configuration
357
+ */
358
+ export const DEFAULT_WORK_STEALING_CONFIG: WorkStealingConfig = {
359
+ staleThresholdMinutes: 30,
360
+ blockedThresholdMinutes: 60,
361
+ overloadThreshold: 5,
362
+ gracePeriodMinutes: 10,
363
+ minProgressToProtect: 75,
364
+ contestWindowMinutes: 5,
365
+ allowCrossTypeSteal: true,
366
+ crossTypeStealRules: [
367
+ ['coder', 'debugger'],
368
+ ['tester', 'reviewer'],
369
+ ],
370
+ };
371
+
372
+ /**
373
+ * Extended IssueClaim with work stealing properties
374
+ */
375
+ export interface IssueClaimWithStealing extends IssueClaim {
376
+ progress: number; // 0-100 percentage
377
+ blockedReason?: string;
378
+ blockedAt?: Date;
379
+ stealableAt?: Date;
380
+ stealInfo?: StealableInfo;
381
+ contestInfo?: ContestInfo;
382
+ }
383
+
384
+ // =============================================================================
385
+ // Work Stealing Events
386
+ // =============================================================================
387
+
388
+ /**
389
+ * Event types for work stealing
390
+ */
391
+ export type WorkStealingEventType =
392
+ | 'IssueMarkedStealable'
393
+ | 'IssueStolen'
394
+ | 'StealContested'
395
+ | 'StealContestResolved';
396
+
397
+ /**
398
+ * Base event interface for work stealing
399
+ */
400
+ export interface WorkStealingEvent {
401
+ id: string;
402
+ type: WorkStealingEventType;
403
+ timestamp: Date;
404
+ issueId: IssueId;
405
+ claimId: ClaimId;
406
+ payload: unknown;
407
+ }
408
+
409
+ /**
410
+ * Event: Issue marked as stealable
411
+ */
412
+ export interface IssueMarkedStealableEvent extends WorkStealingEvent {
413
+ type: 'IssueMarkedStealable';
414
+ payload: {
415
+ info: StealableInfo;
416
+ currentClaimant: Claimant;
417
+ claim: IssueClaimWithStealing;
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Event: Issue stolen
423
+ */
424
+ export interface IssueStolenEvent extends WorkStealingEvent {
425
+ type: 'IssueStolen';
426
+ payload: {
427
+ previousClaimant: Claimant;
428
+ newClaimant: Claimant;
429
+ stealableInfo: StealableInfo;
430
+ contestWindowEndsAt: Date;
431
+ };
432
+ }
433
+
434
+ /**
435
+ * Event: Steal contested
436
+ */
437
+ export interface StealContestedEvent extends WorkStealingEvent {
438
+ type: 'StealContested';
439
+ payload: {
440
+ contestInfo: ContestInfo;
441
+ claim: IssueClaimWithStealing;
442
+ };
443
+ }
444
+
445
+ /**
446
+ * Event: Contest resolved
447
+ */
448
+ export interface StealContestResolvedEvent extends WorkStealingEvent {
449
+ type: 'StealContestResolved';
450
+ payload: {
451
+ contestInfo: ContestInfo;
452
+ resolution: ContestResolution;
453
+ winnerClaim: IssueClaimWithStealing;
454
+ };
455
+ }
456
+
457
+ // =============================================================================
458
+ // Repository Interface for Work Stealing
459
+ // =============================================================================
460
+
461
+ /**
462
+ * Repository interface for issue claims with work stealing support
463
+ */
464
+ export interface IIssueClaimRepository {
465
+ findById(id: ClaimId): Promise<IssueClaimWithStealing | null>;
466
+ findByIssueId(issueId: IssueId): Promise<IssueClaimWithStealing | null>;
467
+ findByAgentId(agentId: string): Promise<IssueClaimWithStealing[]>;
468
+ findStealable(agentType?: AgentType): Promise<IssueClaimWithStealing[]>;
469
+ findContested(): Promise<IssueClaimWithStealing[]>;
470
+ findAll(): Promise<IssueClaimWithStealing[]>;
471
+ save(claim: IssueClaimWithStealing): Promise<void>;
472
+ update(claim: IssueClaimWithStealing): Promise<void>;
473
+ delete(id: ClaimId): Promise<void>;
474
+ countByAgentId(agentId: string): Promise<number>;
475
+ }
476
+
477
+ // =============================================================================
478
+ // Event Bus Interface for Work Stealing
479
+ // =============================================================================
480
+
481
+ /**
482
+ * Event bus interface for work stealing events
483
+ */
484
+ export interface IWorkStealingEventBus {
485
+ emit(event: WorkStealingEvent): Promise<void>;
486
+ subscribe(
487
+ eventType: WorkStealingEventType,
488
+ handler: (event: WorkStealingEvent) => void | Promise<void>
489
+ ): () => void;
490
+ subscribeAll(handler: (event: WorkStealingEvent) => void | Promise<void>): () => void;
491
+ }
492
+
493
+ // =============================================================================
494
+ // ADR-016 Extended Types
495
+ // =============================================================================
496
+
497
+ /**
498
+ * Extended claim status per ADR-016
499
+ * Includes all lifecycle states for human/agent claiming
500
+ */
501
+ export type ExtendedClaimStatus =
502
+ | 'active' // Claim is being actively worked on
503
+ | 'paused' // Work is temporarily paused
504
+ | 'handoff-pending' // Handoff has been requested, awaiting acceptance
505
+ | 'review-requested' // Claimant is requesting a review
506
+ | 'blocked' // Work is blocked by external dependency
507
+ | 'stealable' // Claim can be stolen by another agent
508
+ | 'completed'; // Work is finished
509
+
510
+ /**
511
+ * Agent identifier type
512
+ */
513
+ export type AgentId = `agent-${string}` | string;
514
+
515
+ /**
516
+ * Human user identifier
517
+ */
518
+ export type UserId = string;
519
+
520
+ /**
521
+ * Reasons why a claim might be blocked
522
+ */
523
+ export type BlockedReason =
524
+ | 'dependency' // Waiting on another issue/PR
525
+ | 'external' // Waiting on external resource
526
+ | 'clarification' // Waiting for clarification from maintainer
527
+ | 'resource' // Waiting for computational resource
528
+ | 'approval' // Waiting for approval
529
+ | 'other'; // Other reason
530
+
531
+ /**
532
+ * Information about a blocked claim
533
+ */
534
+ export interface BlockedInfo {
535
+ readonly reason: BlockedReason;
536
+ readonly description: string;
537
+ readonly relatedIssues: readonly IssueId[];
538
+ readonly blockedAt: number;
539
+ readonly estimatedUnblockTime?: number;
540
+ }
541
+
542
+ /**
543
+ * Reason for work stealing per ADR-016
544
+ */
545
+ export type StealReason =
546
+ | 'timeout' // Original claimant timed out
547
+ | 'overloaded' // Original claimant is overloaded
548
+ | 'blocked' // Claim has been blocked too long
549
+ | 'voluntary' // Original claimant voluntarily released
550
+ | 'rebalancing' // System is rebalancing load
551
+ | 'abandoned' // Claimant appears to have abandoned work
552
+ | 'priority-change'; // Higher priority claimant needs this issue
553
+
554
+ /**
555
+ * Extended StealableInfo per ADR-016
556
+ */
557
+ export interface ExtendedStealableInfo {
558
+ readonly reason: StealReason;
559
+ readonly markedAt: number;
560
+ readonly originalClaimant: Claimant;
561
+ readonly minPriorityToSteal: IssuePriority;
562
+ readonly requiresContest: boolean;
563
+ readonly gracePeriodMs: number;
564
+ readonly gracePeriodEndsAt: number;
565
+ readonly previousSteals: number;
566
+ readonly interestedAgents: readonly AgentId[];
567
+ }
568
+
569
+ /**
570
+ * Handoff reason per ADR-016
571
+ */
572
+ export type HandoffReason =
573
+ | 'capacity' // Current claimant at capacity
574
+ | 'expertise' // Target has better expertise
575
+ | 'shift-change' // Scheduled handoff
576
+ | 'escalation' // Issue needs escalation
577
+ | 'voluntary' // Voluntary transfer
578
+ | 'rebalancing'; // System rebalancing
579
+
580
+ /**
581
+ * Extended handoff info per ADR-016
582
+ */
583
+ export interface ExtendedHandoffInfo {
584
+ readonly initiatedBy: AgentId | UserId;
585
+ readonly targetClaimant: Claimant;
586
+ readonly requestedAt: number;
587
+ readonly expiresAt: number;
588
+ readonly reason: HandoffReason;
589
+ readonly notes?: string;
590
+ readonly contextSummary?: string;
591
+ readonly artifacts: readonly string[];
592
+ }
593
+
594
+ /**
595
+ * Claimant workload metrics per ADR-016
596
+ */
597
+ export interface ClaimantWorkload {
598
+ readonly activeClaims: number;
599
+ readonly pausedClaims: number;
600
+ readonly pendingHandoffs: number;
601
+ readonly completedClaims: number;
602
+ readonly averageCompletionTime: number;
603
+ readonly loadPercentage: number;
604
+ readonly availableCapacity: number;
605
+ }
606
+
607
+ /**
608
+ * Extended Claimant with ADR-016 properties
609
+ */
610
+ export interface ExtendedClaimant extends Claimant {
611
+ readonly workload: ClaimantWorkload;
612
+ readonly priority: number;
613
+ readonly registeredAt: number;
614
+ readonly lastActivityAt: number;
615
+ readonly isAvailable: boolean;
616
+ }
617
+
618
+ /**
619
+ * Agent load information per ADR-016
620
+ */
621
+ export interface AgentLoadInfo {
622
+ readonly agentId: AgentId;
623
+ readonly name: string;
624
+ readonly activeClaims: number;
625
+ readonly maxClaims: number;
626
+ readonly loadPercentage: number;
627
+ readonly isOverloaded: boolean;
628
+ readonly isUnderloaded: boolean;
629
+ readonly capabilities: readonly string[];
630
+ readonly processingRate: number;
631
+ readonly avgClaimDuration: number;
632
+ readonly queueDepth: number;
633
+ readonly healthScore: number;
634
+ }
635
+
636
+ /**
637
+ * Claim move during rebalancing
638
+ */
639
+ export interface ClaimMove {
640
+ readonly claimId: ClaimId;
641
+ readonly fromAgent: AgentId;
642
+ readonly toAgent: AgentId;
643
+ readonly reason: string;
644
+ readonly success: boolean;
645
+ }
646
+
647
+ /**
648
+ * Rebalancing error
649
+ */
650
+ export interface RebalanceError {
651
+ readonly claimId: ClaimId;
652
+ readonly error: string;
653
+ readonly recoverable: boolean;
654
+ }
655
+
656
+ /**
657
+ * Rebalance result per ADR-016
658
+ */
659
+ export interface ExtendedRebalanceResult {
660
+ readonly success: boolean;
661
+ readonly claimsMoved: number;
662
+ readonly moves: readonly ClaimMove[];
663
+ readonly overloadedAgents: readonly AgentId[];
664
+ readonly underloadedAgents: readonly AgentId[];
665
+ readonly loadBefore: readonly AgentLoadInfo[];
666
+ readonly loadAfter: readonly AgentLoadInfo[];
667
+ readonly durationMs: number;
668
+ readonly errors: readonly RebalanceError[];
669
+ readonly timestamp: number;
670
+ }
671
+
672
+ /**
673
+ * Rebalance strategy
674
+ */
675
+ export type RebalanceStrategy =
676
+ | 'oldest-first' // Move oldest claims first
677
+ | 'newest-first' // Move newest claims first
678
+ | 'lowest-priority' // Move lowest priority claims first
679
+ | 'least-progress' // Move claims with least progress first
680
+ | 'capability-match'; // Move to best capability match
681
+
682
+ /**
683
+ * Load balancing configuration per ADR-016
684
+ */
685
+ export interface LoadBalancingConfig {
686
+ readonly enabled: boolean;
687
+ readonly checkIntervalMs: number;
688
+ readonly overloadThreshold: number;
689
+ readonly underloadThreshold: number;
690
+ readonly rebalanceThreshold: number;
691
+ readonly maxMovesPerRebalance: number;
692
+ readonly selectionStrategy: RebalanceStrategy;
693
+ readonly respectCapabilities: boolean;
694
+ readonly cooldownMs: number;
695
+ }
696
+
697
+ /**
698
+ * Default load balancing config
699
+ */
700
+ export const DEFAULT_LOAD_BALANCING_CONFIG: LoadBalancingConfig = {
701
+ enabled: true,
702
+ checkIntervalMs: 5 * 60 * 1000, // 5 minutes
703
+ overloadThreshold: 90,
704
+ underloadThreshold: 30,
705
+ rebalanceThreshold: 40,
706
+ maxMovesPerRebalance: 5,
707
+ selectionStrategy: 'capability-match',
708
+ respectCapabilities: true,
709
+ cooldownMs: 10 * 60 * 1000, // 10 minutes
710
+ };
711
+
712
+ /**
713
+ * Extended IssueClaim with all ADR-016 properties
714
+ */
715
+ export interface ExtendedIssueClaim {
716
+ readonly id: ClaimId;
717
+ readonly issueId: IssueId;
718
+ readonly repository: string;
719
+ readonly claimant: ExtendedClaimant;
720
+ readonly status: ExtendedClaimStatus;
721
+ readonly claimedAt: number;
722
+ readonly updatedAt: number;
723
+ readonly startedAt?: number;
724
+ readonly completedAt?: number;
725
+ readonly expiresAt?: number;
726
+ readonly priority: IssuePriority;
727
+ readonly tags: readonly string[];
728
+ readonly progress: number;
729
+ readonly blockedInfo?: BlockedInfo;
730
+ readonly stealableInfo?: ExtendedStealableInfo;
731
+ readonly handoffInfo?: ExtendedHandoffInfo;
732
+ readonly pullRequestId?: number;
733
+ readonly notes: readonly ClaimNote[];
734
+ readonly statusHistory: readonly StatusChange[];
735
+ readonly metadata: Record<string, unknown>;
736
+ }
737
+
738
+ /**
739
+ * Claim note
740
+ */
741
+ export interface ClaimNote {
742
+ readonly id: string;
743
+ readonly content: string;
744
+ readonly authorId: AgentId | UserId;
745
+ readonly createdAt: number;
746
+ readonly type: 'progress' | 'question' | 'blocker' | 'general';
747
+ }
748
+
749
+ /**
750
+ * Status change record
751
+ */
752
+ export interface StatusChange {
753
+ readonly fromStatus: ExtendedClaimStatus;
754
+ readonly toStatus: ExtendedClaimStatus;
755
+ readonly changedAt: number;
756
+ readonly changedBy: AgentId | UserId;
757
+ readonly reason?: string;
758
+ }
759
+
760
+ /**
761
+ * Query options for claims
762
+ */
763
+ export interface ClaimQueryOptions {
764
+ readonly claimantId?: AgentId | UserId;
765
+ readonly claimantType?: ClaimantType;
766
+ readonly status?: ExtendedClaimStatus | readonly ExtendedClaimStatus[];
767
+ readonly repository?: string;
768
+ readonly issueId?: IssueId;
769
+ readonly priority?: IssuePriority | readonly IssuePriority[];
770
+ readonly tags?: readonly string[];
771
+ readonly createdAfter?: number;
772
+ readonly createdBefore?: number;
773
+ readonly updatedAfter?: number;
774
+ readonly stealableOnly?: boolean;
775
+ readonly blockedOnly?: boolean;
776
+ readonly limit?: number;
777
+ readonly offset?: number;
778
+ readonly sortBy?: 'claimedAt' | 'updatedAt' | 'priority' | 'progress';
779
+ readonly sortDirection?: 'asc' | 'desc';
780
+ }
781
+
782
+ /**
783
+ * Claim statistics
784
+ */
785
+ export interface ClaimStatistics {
786
+ readonly totalClaims: number;
787
+ readonly byStatus: Record<ExtendedClaimStatus, number>;
788
+ readonly byPriority: Record<IssuePriority, number>;
789
+ readonly byClaimantType: Record<ClaimantType, number>;
790
+ readonly avgDurationMs: number;
791
+ readonly avgProgress: number;
792
+ readonly activeSteals: number;
793
+ readonly pendingHandoffs: number;
794
+ readonly completedLast24h: number;
795
+ readonly byRepository: Record<string, number>;
796
+ }
797
+
798
+ /**
799
+ * Helper function to generate unique claim IDs
800
+ */
801
+ export function generateClaimId(): ClaimId {
802
+ return `claim-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
803
+ }
804
+
805
+ /**
806
+ * Check if a status is an active claim status
807
+ */
808
+ export function isActiveClaimStatus(status: ExtendedClaimStatus): boolean {
809
+ return ['active', 'paused', 'blocked', 'handoff-pending', 'review-requested'].includes(status);
810
+ }
811
+
812
+ /**
813
+ * Get valid status transitions per ADR-016
814
+ */
815
+ export function getValidStatusTransitions(currentStatus: ExtendedClaimStatus): readonly ExtendedClaimStatus[] {
816
+ const transitions: Record<ExtendedClaimStatus, readonly ExtendedClaimStatus[]> = {
817
+ 'active': ['paused', 'blocked', 'handoff-pending', 'review-requested', 'stealable', 'completed'],
818
+ 'paused': ['active', 'blocked', 'handoff-pending', 'stealable', 'completed'],
819
+ 'blocked': ['active', 'paused', 'stealable', 'completed'],
820
+ 'handoff-pending': ['active', 'completed'],
821
+ 'review-requested': ['active', 'completed', 'blocked'],
822
+ 'stealable': ['active', 'completed'],
823
+ 'completed': [],
824
+ };
825
+ return transitions[currentStatus];
826
+ }