github-issue-tower-defence-management 1.54.0 → 1.56.0
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/CHANGELOG.md +14 -0
- package/README.md +23 -0
- package/bin/adapter/entry-points/cli/index.js +3 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +5 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/proxy/RateLimitCache.js +123 -0
- package/bin/adapter/proxy/RateLimitCache.js.map +1 -0
- package/bin/adapter/proxy/TokenListLoader.js +72 -0
- package/bin/adapter/proxy/TokenListLoader.js.map +1 -0
- package/bin/adapter/proxy/ensureProxyRunning.js +73 -0
- package/bin/adapter/proxy/ensureProxyRunning.js.map +1 -0
- package/bin/adapter/proxy/proxyEntry.js +96 -0
- package/bin/adapter/proxy/proxyEntry.js.map +1 -0
- package/bin/adapter/repositories/NodeLocalCommandRunner.js +10 -4
- package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +35 -0
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -0
- package/bin/domain/entities/ClaudeTokenUsage.js +3 -0
- package/bin/domain/entities/ClaudeTokenUsage.js.map +1 -0
- package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js +1 -1
- package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +26 -2
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/ClaudeTokenUsageRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/ClaudeTokenUsageRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +5 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +13 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +5 -0
- package/src/adapter/proxy/RateLimitCache.test.ts +131 -0
- package/src/adapter/proxy/RateLimitCache.ts +112 -0
- package/src/adapter/proxy/TokenListLoader.test.ts +82 -0
- package/src/adapter/proxy/TokenListLoader.ts +35 -0
- package/src/adapter/proxy/ensureProxyRunning.test.ts +85 -0
- package/src/adapter/proxy/ensureProxyRunning.ts +41 -0
- package/src/adapter/proxy/proxyEntry.test.ts +48 -0
- package/src/adapter/proxy/proxyEntry.ts +69 -0
- package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +3 -1
- package/src/adapter/repositories/NodeLocalCommandRunner.ts +18 -4
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +127 -0
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +36 -0
- package/src/domain/entities/ClaudeTokenUsage.ts +5 -0
- package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.test.ts +26 -15
- package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.ts +3 -1
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +308 -0
- package/src/domain/usecases/StartPreparationUseCase.ts +37 -1
- package/src/domain/usecases/adapter-interfaces/ClaudeTokenUsageRepository.ts +7 -0
- package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +5 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/proxy/RateLimitCache.d.ts +14 -0
- package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -0
- package/types/adapter/proxy/TokenListLoader.d.ts +2 -0
- package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -0
- package/types/adapter/proxy/ensureProxyRunning.d.ts +2 -0
- package/types/adapter/proxy/ensureProxyRunning.d.ts.map +1 -0
- package/types/adapter/proxy/proxyEntry.d.ts +4 -0
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -0
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +2 -2
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts +11 -0
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts +6 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -0
- package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts +2 -0
- package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +4 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ClaudeTokenUsageRepository.d.ts +7 -0
- package/types/domain/usecases/adapter-interfaces/ClaudeTokenUsageRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +4 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
|
@@ -100,6 +100,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
100
100
|
cacheUsed: boolean;
|
|
101
101
|
urlOfStoryView: string;
|
|
102
102
|
storyObjectMap: StoryObjectMap;
|
|
103
|
+
manager: string;
|
|
103
104
|
};
|
|
104
105
|
expectedThrowError?: Error;
|
|
105
106
|
expectedCreateNewIssueCalls: [
|
|
@@ -122,6 +123,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
122
123
|
cacheUsed: false,
|
|
123
124
|
urlOfStoryView: 'https://example.com',
|
|
124
125
|
storyObjectMap: basicStoryObjectMap,
|
|
126
|
+
manager: 'manager',
|
|
125
127
|
},
|
|
126
128
|
expectedCreateNewIssueCalls: [],
|
|
127
129
|
expectedUpdateIssueCalls: [],
|
|
@@ -136,6 +138,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
136
138
|
cacheUsed: true,
|
|
137
139
|
urlOfStoryView: 'https://example.com',
|
|
138
140
|
storyObjectMap: basicStoryObjectMap,
|
|
141
|
+
manager: 'manager',
|
|
139
142
|
},
|
|
140
143
|
expectedCreateNewIssueCalls: [
|
|
141
144
|
[
|
|
@@ -143,7 +146,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
143
146
|
'repo',
|
|
144
147
|
'Task 1',
|
|
145
148
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
146
|
-
[],
|
|
149
|
+
['manager'],
|
|
147
150
|
[],
|
|
148
151
|
],
|
|
149
152
|
[
|
|
@@ -151,7 +154,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
151
154
|
'repo',
|
|
152
155
|
'Task 2',
|
|
153
156
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
154
|
-
[],
|
|
157
|
+
['manager'],
|
|
155
158
|
[],
|
|
156
159
|
],
|
|
157
160
|
[
|
|
@@ -159,7 +162,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
159
162
|
'repo',
|
|
160
163
|
'Task 3',
|
|
161
164
|
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
162
|
-
[],
|
|
165
|
+
['manager'],
|
|
163
166
|
[],
|
|
164
167
|
],
|
|
165
168
|
[
|
|
@@ -167,7 +170,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
167
170
|
'repo',
|
|
168
171
|
'Task 4',
|
|
169
172
|
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
170
|
-
[],
|
|
173
|
+
['manager'],
|
|
171
174
|
[],
|
|
172
175
|
],
|
|
173
176
|
],
|
|
@@ -278,6 +281,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
278
281
|
cacheUsed: false,
|
|
279
282
|
urlOfStoryView: 'https://example.com',
|
|
280
283
|
storyObjectMap: regularStoryObjectMap,
|
|
284
|
+
manager: 'manager',
|
|
281
285
|
},
|
|
282
286
|
expectedCreateNewIssueCalls: [],
|
|
283
287
|
expectedUpdateIssueCalls: [],
|
|
@@ -292,6 +296,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
292
296
|
cacheUsed: false,
|
|
293
297
|
urlOfStoryView: 'https://example.com',
|
|
294
298
|
storyObjectMap: basicStoryObjectMap,
|
|
299
|
+
manager: 'manager',
|
|
295
300
|
},
|
|
296
301
|
expectedThrowError: new Error('Story issue not found: Story 1'),
|
|
297
302
|
expectedCreateNewIssueCalls: [],
|
|
@@ -318,6 +323,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
318
323
|
cacheUsed: false,
|
|
319
324
|
urlOfStoryView: 'https://example.com',
|
|
320
325
|
storyObjectMap: basicStoryObjectMap,
|
|
326
|
+
manager: 'manager',
|
|
321
327
|
},
|
|
322
328
|
expectedCreateNewIssueCalls: [],
|
|
323
329
|
expectedUpdateIssueCalls: [],
|
|
@@ -332,6 +338,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
332
338
|
cacheUsed: false,
|
|
333
339
|
urlOfStoryView: 'https://example.com',
|
|
334
340
|
storyObjectMap: basicStoryObjectMap,
|
|
341
|
+
manager: 'manager',
|
|
335
342
|
},
|
|
336
343
|
expectedCreateNewIssueCalls: [
|
|
337
344
|
[
|
|
@@ -339,7 +346,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
339
346
|
'repo',
|
|
340
347
|
'Task 1',
|
|
341
348
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
342
|
-
[],
|
|
349
|
+
['manager'],
|
|
343
350
|
[],
|
|
344
351
|
],
|
|
345
352
|
[
|
|
@@ -347,7 +354,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
347
354
|
'repo',
|
|
348
355
|
'Task 2',
|
|
349
356
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
350
|
-
[],
|
|
357
|
+
['manager'],
|
|
351
358
|
[],
|
|
352
359
|
],
|
|
353
360
|
[
|
|
@@ -355,7 +362,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
355
362
|
'repo',
|
|
356
363
|
'Task 3',
|
|
357
364
|
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
358
|
-
[],
|
|
365
|
+
['manager'],
|
|
359
366
|
[],
|
|
360
367
|
],
|
|
361
368
|
[
|
|
@@ -363,7 +370,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
363
370
|
'repo',
|
|
364
371
|
'Task 4',
|
|
365
372
|
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
366
|
-
[],
|
|
373
|
+
['manager'],
|
|
367
374
|
[],
|
|
368
375
|
],
|
|
369
376
|
],
|
|
@@ -489,6 +496,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
489
496
|
cacheUsed: false,
|
|
490
497
|
urlOfStoryView: 'https://example.com',
|
|
491
498
|
storyObjectMap: new Map([['Story 1', basicStoryObject1]]),
|
|
499
|
+
manager: 'manager',
|
|
492
500
|
},
|
|
493
501
|
expectedCreateNewIssueCalls: [
|
|
494
502
|
[
|
|
@@ -496,7 +504,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
496
504
|
'repo',
|
|
497
505
|
'Task 1',
|
|
498
506
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
499
|
-
[],
|
|
507
|
+
['manager'],
|
|
500
508
|
[],
|
|
501
509
|
],
|
|
502
510
|
],
|
|
@@ -565,6 +573,7 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
565
573
|
},
|
|
566
574
|
],
|
|
567
575
|
]),
|
|
576
|
+
manager: 'manager',
|
|
568
577
|
},
|
|
569
578
|
expectedCreateNewIssueCalls: [],
|
|
570
579
|
expectedUpdateIssueCalls: [
|
|
@@ -597,6 +606,7 @@ Some description without checkboxes`,
|
|
|
597
606
|
cacheUsed: false,
|
|
598
607
|
urlOfStoryView: 'https://example.com',
|
|
599
608
|
storyObjectMap: basicStoryObjectMap,
|
|
609
|
+
manager: 'manager',
|
|
600
610
|
},
|
|
601
611
|
expectedCreateNewIssueCalls: [
|
|
602
612
|
[
|
|
@@ -604,7 +614,7 @@ Some description without checkboxes`,
|
|
|
604
614
|
'repo',
|
|
605
615
|
'Task 1',
|
|
606
616
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
607
|
-
[],
|
|
617
|
+
['manager'],
|
|
608
618
|
[],
|
|
609
619
|
],
|
|
610
620
|
[
|
|
@@ -612,7 +622,7 @@ Some description without checkboxes`,
|
|
|
612
622
|
'repo',
|
|
613
623
|
'Task 2 for `Story 1 #123`',
|
|
614
624
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
615
|
-
[],
|
|
625
|
+
['manager'],
|
|
616
626
|
[],
|
|
617
627
|
],
|
|
618
628
|
[
|
|
@@ -620,7 +630,7 @@ Some description without checkboxes`,
|
|
|
620
630
|
'repo',
|
|
621
631
|
'Task 3',
|
|
622
632
|
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
623
|
-
[],
|
|
633
|
+
['manager'],
|
|
624
634
|
[],
|
|
625
635
|
],
|
|
626
636
|
[
|
|
@@ -628,7 +638,7 @@ Some description without checkboxes`,
|
|
|
628
638
|
'repo',
|
|
629
639
|
'Task 4',
|
|
630
640
|
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
631
|
-
[],
|
|
641
|
+
['manager'],
|
|
632
642
|
[],
|
|
633
643
|
],
|
|
634
644
|
],
|
|
@@ -787,6 +797,7 @@ Some description without checkboxes`,
|
|
|
787
797
|
},
|
|
788
798
|
],
|
|
789
799
|
]),
|
|
800
|
+
manager: 'manager',
|
|
790
801
|
},
|
|
791
802
|
expectedCreateNewIssueCalls: [
|
|
792
803
|
[
|
|
@@ -794,7 +805,7 @@ Some description without checkboxes`,
|
|
|
794
805
|
'repoA',
|
|
795
806
|
'Task 1',
|
|
796
807
|
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
797
|
-
[],
|
|
808
|
+
['manager'],
|
|
798
809
|
[],
|
|
799
810
|
],
|
|
800
811
|
[
|
|
@@ -802,7 +813,7 @@ Some description without checkboxes`,
|
|
|
802
813
|
'repoB',
|
|
803
814
|
'Task 2',
|
|
804
815
|
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
805
|
-
[],
|
|
816
|
+
['manager'],
|
|
806
817
|
[],
|
|
807
818
|
],
|
|
808
819
|
],
|
|
@@ -4,6 +4,7 @@ import { Project } from '../entities/Project';
|
|
|
4
4
|
import { StoryObjectMap } from '../entities/StoryObjectMap';
|
|
5
5
|
import { encodeForURI } from './utils';
|
|
6
6
|
import { ICEBOX_STATUS_NAME } from '../entities/WorkflowStatus';
|
|
7
|
+
import { Member } from '../entities/Member';
|
|
7
8
|
|
|
8
9
|
export class ConvertCheckboxToIssueInStoryIssueUseCase {
|
|
9
10
|
constructor(
|
|
@@ -19,6 +20,7 @@ export class ConvertCheckboxToIssueInStoryIssueUseCase {
|
|
|
19
20
|
cacheUsed: boolean;
|
|
20
21
|
urlOfStoryView: string;
|
|
21
22
|
storyObjectMap: StoryObjectMap;
|
|
23
|
+
manager: Member['name'];
|
|
22
24
|
}): Promise<void> => {
|
|
23
25
|
const story = input.project.story;
|
|
24
26
|
if (!story) {
|
|
@@ -74,7 +76,7 @@ export class ConvertCheckboxToIssueInStoryIssueUseCase {
|
|
|
74
76
|
freshStoryIssue.repo,
|
|
75
77
|
issueTitle,
|
|
76
78
|
newIssueBody,
|
|
77
|
-
[],
|
|
79
|
+
[input.manager],
|
|
78
80
|
[],
|
|
79
81
|
);
|
|
80
82
|
const newIssueUrl = `https://github.com/${freshStoryIssue.org}/${freshStoryIssue.repo}/issues/${newIssueNumber}`;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
7
7
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
8
8
|
import { ClaudeRepository } from './adapter-interfaces/ClaudeRepository';
|
|
9
|
+
import { ClaudeTokenUsageRepository } from './adapter-interfaces/ClaudeTokenUsageRepository';
|
|
9
10
|
import { Issue } from '../entities/Issue';
|
|
10
11
|
import { Project } from '../entities/Project';
|
|
11
12
|
import { StoryObjectMap } from '../entities/StoryObjectMap';
|
|
@@ -92,6 +93,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
92
93
|
>;
|
|
93
94
|
let mockClaudeRepository: Mocked<Pick<ClaudeRepository, 'getUsage'>>;
|
|
94
95
|
let mockLocalCommandRunner: Mocked<LocalCommandRunner>;
|
|
96
|
+
let mockClaudeTokenUsageRepository: Mocked<ClaudeTokenUsageRepository>;
|
|
95
97
|
let mockProject: Project;
|
|
96
98
|
beforeEach(() => {
|
|
97
99
|
jest.resetAllMocks();
|
|
@@ -114,11 +116,17 @@ describe('StartPreparationUseCase', () => {
|
|
|
114
116
|
mockLocalCommandRunner = {
|
|
115
117
|
runCommand: jest.fn(),
|
|
116
118
|
};
|
|
119
|
+
mockClaudeTokenUsageRepository = {
|
|
120
|
+
ensureObservable: jest.fn().mockResolvedValue(undefined),
|
|
121
|
+
getAvailableTokenUsages: jest.fn().mockResolvedValue([]),
|
|
122
|
+
proxyBaseUrl: jest.fn().mockReturnValue('http://127.0.0.1:8787'),
|
|
123
|
+
};
|
|
117
124
|
useCase = new StartPreparationUseCase(
|
|
118
125
|
mockProjectRepository,
|
|
119
126
|
mockIssueRepository,
|
|
120
127
|
mockClaudeRepository,
|
|
121
128
|
mockLocalCommandRunner,
|
|
129
|
+
mockClaudeTokenUsageRepository,
|
|
122
130
|
);
|
|
123
131
|
});
|
|
124
132
|
it('should run aw command for awaiting workspace issues', async () => {
|
|
@@ -2516,4 +2524,304 @@ describe('StartPreparationUseCase', () => {
|
|
|
2516
2524
|
url: 'https://github.com/user/repo/issues/2',
|
|
2517
2525
|
});
|
|
2518
2526
|
});
|
|
2527
|
+
|
|
2528
|
+
it('should pass CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_BASE_URL to runCommand when tokens are available', async () => {
|
|
2529
|
+
const awaitingIssue = createMockIssue({
|
|
2530
|
+
url: 'url1',
|
|
2531
|
+
title: 'Issue 1',
|
|
2532
|
+
labels: ['category:impl'],
|
|
2533
|
+
status: 'Awaiting Workspace',
|
|
2534
|
+
number: 1,
|
|
2535
|
+
itemId: 'item-1',
|
|
2536
|
+
});
|
|
2537
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2538
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2539
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
2540
|
+
);
|
|
2541
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2542
|
+
stdout: '',
|
|
2543
|
+
stderr: '',
|
|
2544
|
+
exitCode: 0,
|
|
2545
|
+
});
|
|
2546
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2547
|
+
{ token: 'token-a', fiveHourUtilization: 0, blocked: false },
|
|
2548
|
+
]);
|
|
2549
|
+
|
|
2550
|
+
await useCase.run({
|
|
2551
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2552
|
+
defaultAgentName: 'agent1',
|
|
2553
|
+
defaultLlmModelName: 'claude-opus',
|
|
2554
|
+
defaultLlmAgentName: null,
|
|
2555
|
+
configFilePath: '/path/to/config.yml',
|
|
2556
|
+
maximumPreparingIssuesCount: null,
|
|
2557
|
+
utilizationPercentageThreshold: 90,
|
|
2558
|
+
allowedIssueAuthors: null,
|
|
2559
|
+
codexHomeCandidates: null,
|
|
2560
|
+
allowIssueCacheMinutes: 0,
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2563
|
+
expect(
|
|
2564
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mock.calls,
|
|
2565
|
+
).toHaveLength(1);
|
|
2566
|
+
expect(
|
|
2567
|
+
mockClaudeTokenUsageRepository.ensureObservable.mock.calls,
|
|
2568
|
+
).toHaveLength(1);
|
|
2569
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2570
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
2571
|
+
env: {
|
|
2572
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-a',
|
|
2573
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
2574
|
+
},
|
|
2575
|
+
});
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
it('should rotate Claude OAuth tokens round-robin across multiple awaiting issues', async () => {
|
|
2579
|
+
const awaitingIssues: Issue[] = [
|
|
2580
|
+
createMockIssue({
|
|
2581
|
+
url: 'url1',
|
|
2582
|
+
title: 'Issue 1',
|
|
2583
|
+
labels: ['category:impl'],
|
|
2584
|
+
status: 'Awaiting Workspace',
|
|
2585
|
+
number: 1,
|
|
2586
|
+
itemId: 'item-1',
|
|
2587
|
+
}),
|
|
2588
|
+
createMockIssue({
|
|
2589
|
+
url: 'url2',
|
|
2590
|
+
title: 'Issue 2',
|
|
2591
|
+
labels: ['category:impl'],
|
|
2592
|
+
status: 'Awaiting Workspace',
|
|
2593
|
+
number: 2,
|
|
2594
|
+
itemId: 'item-2',
|
|
2595
|
+
}),
|
|
2596
|
+
createMockIssue({
|
|
2597
|
+
url: 'url3',
|
|
2598
|
+
title: 'Issue 3',
|
|
2599
|
+
labels: ['category:impl'],
|
|
2600
|
+
status: 'Awaiting Workspace',
|
|
2601
|
+
number: 3,
|
|
2602
|
+
itemId: 'item-3',
|
|
2603
|
+
}),
|
|
2604
|
+
];
|
|
2605
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2606
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2607
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
2608
|
+
);
|
|
2609
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2610
|
+
stdout: '',
|
|
2611
|
+
stderr: '',
|
|
2612
|
+
exitCode: 0,
|
|
2613
|
+
});
|
|
2614
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2615
|
+
{ token: 'token-a', fiveHourUtilization: 0, blocked: false },
|
|
2616
|
+
{ token: 'token-b', fiveHourUtilization: 0, blocked: false },
|
|
2617
|
+
]);
|
|
2618
|
+
|
|
2619
|
+
await useCase.run({
|
|
2620
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2621
|
+
defaultAgentName: 'agent1',
|
|
2622
|
+
defaultLlmModelName: 'claude-opus',
|
|
2623
|
+
defaultLlmAgentName: null,
|
|
2624
|
+
configFilePath: '/path/to/config.yml',
|
|
2625
|
+
maximumPreparingIssuesCount: null,
|
|
2626
|
+
utilizationPercentageThreshold: 90,
|
|
2627
|
+
allowedIssueAuthors: null,
|
|
2628
|
+
codexHomeCandidates: null,
|
|
2629
|
+
allowIssueCacheMinutes: 0,
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
2633
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toMatchObject({
|
|
2634
|
+
env: {
|
|
2635
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-a',
|
|
2636
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
2637
|
+
},
|
|
2638
|
+
});
|
|
2639
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[1][2]).toMatchObject({
|
|
2640
|
+
env: {
|
|
2641
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-b',
|
|
2642
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
2643
|
+
},
|
|
2644
|
+
});
|
|
2645
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[2][2]).toMatchObject({
|
|
2646
|
+
env: {
|
|
2647
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-a',
|
|
2648
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
2649
|
+
},
|
|
2650
|
+
});
|
|
2651
|
+
});
|
|
2652
|
+
|
|
2653
|
+
it('should not inject env when no tokens are available', async () => {
|
|
2654
|
+
const awaitingIssue = createMockIssue({
|
|
2655
|
+
url: 'url1',
|
|
2656
|
+
title: 'Issue 1',
|
|
2657
|
+
labels: ['category:impl'],
|
|
2658
|
+
status: 'Awaiting Workspace',
|
|
2659
|
+
number: 1,
|
|
2660
|
+
itemId: 'item-1',
|
|
2661
|
+
});
|
|
2662
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2663
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2664
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
2665
|
+
);
|
|
2666
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2667
|
+
stdout: '',
|
|
2668
|
+
stderr: '',
|
|
2669
|
+
exitCode: 0,
|
|
2670
|
+
});
|
|
2671
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue(
|
|
2672
|
+
[],
|
|
2673
|
+
);
|
|
2674
|
+
|
|
2675
|
+
await useCase.run({
|
|
2676
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2677
|
+
defaultAgentName: 'agent1',
|
|
2678
|
+
defaultLlmModelName: 'claude-opus',
|
|
2679
|
+
defaultLlmAgentName: null,
|
|
2680
|
+
configFilePath: '/path/to/config.yml',
|
|
2681
|
+
maximumPreparingIssuesCount: null,
|
|
2682
|
+
utilizationPercentageThreshold: 90,
|
|
2683
|
+
allowedIssueAuthors: null,
|
|
2684
|
+
codexHomeCandidates: null,
|
|
2685
|
+
allowIssueCacheMinutes: 0,
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2689
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toBeUndefined();
|
|
2690
|
+
expect(
|
|
2691
|
+
mockClaudeTokenUsageRepository.ensureObservable.mock.calls,
|
|
2692
|
+
).toHaveLength(0);
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
it('should pick the least-utilized token first', async () => {
|
|
2696
|
+
const awaitingIssue = createMockIssue({
|
|
2697
|
+
url: 'url1',
|
|
2698
|
+
title: 'Issue 1',
|
|
2699
|
+
labels: ['category:impl'],
|
|
2700
|
+
status: 'Awaiting Workspace',
|
|
2701
|
+
number: 1,
|
|
2702
|
+
itemId: 'item-1',
|
|
2703
|
+
});
|
|
2704
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2705
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2706
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
2707
|
+
);
|
|
2708
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2709
|
+
stdout: '',
|
|
2710
|
+
stderr: '',
|
|
2711
|
+
exitCode: 0,
|
|
2712
|
+
});
|
|
2713
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2714
|
+
{ token: 'token-high', fiveHourUtilization: 80, blocked: false },
|
|
2715
|
+
{ token: 'token-low', fiveHourUtilization: 10, blocked: false },
|
|
2716
|
+
]);
|
|
2717
|
+
|
|
2718
|
+
await useCase.run({
|
|
2719
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2720
|
+
defaultAgentName: 'agent1',
|
|
2721
|
+
defaultLlmModelName: 'claude-opus',
|
|
2722
|
+
defaultLlmAgentName: null,
|
|
2723
|
+
configFilePath: '/path/to/config.yml',
|
|
2724
|
+
maximumPreparingIssuesCount: null,
|
|
2725
|
+
utilizationPercentageThreshold: 90,
|
|
2726
|
+
allowedIssueAuthors: null,
|
|
2727
|
+
codexHomeCandidates: null,
|
|
2728
|
+
allowIssueCacheMinutes: 0,
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2732
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
2733
|
+
env: {
|
|
2734
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-low',
|
|
2735
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
2736
|
+
},
|
|
2737
|
+
});
|
|
2738
|
+
});
|
|
2739
|
+
|
|
2740
|
+
it('should exclude blocked tokens from rotation', async () => {
|
|
2741
|
+
const awaitingIssue = createMockIssue({
|
|
2742
|
+
url: 'url1',
|
|
2743
|
+
title: 'Issue 1',
|
|
2744
|
+
labels: ['category:impl'],
|
|
2745
|
+
status: 'Awaiting Workspace',
|
|
2746
|
+
number: 1,
|
|
2747
|
+
itemId: 'item-1',
|
|
2748
|
+
});
|
|
2749
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2750
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2751
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
2752
|
+
);
|
|
2753
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2754
|
+
stdout: '',
|
|
2755
|
+
stderr: '',
|
|
2756
|
+
exitCode: 0,
|
|
2757
|
+
});
|
|
2758
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2759
|
+
{ token: 'token-blocked', fiveHourUtilization: 5, blocked: true },
|
|
2760
|
+
{ token: 'token-ok', fiveHourUtilization: 50, blocked: false },
|
|
2761
|
+
]);
|
|
2762
|
+
|
|
2763
|
+
await useCase.run({
|
|
2764
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2765
|
+
defaultAgentName: 'agent1',
|
|
2766
|
+
defaultLlmModelName: 'claude-opus',
|
|
2767
|
+
defaultLlmAgentName: null,
|
|
2768
|
+
configFilePath: '/path/to/config.yml',
|
|
2769
|
+
maximumPreparingIssuesCount: null,
|
|
2770
|
+
utilizationPercentageThreshold: 90,
|
|
2771
|
+
allowedIssueAuthors: null,
|
|
2772
|
+
codexHomeCandidates: null,
|
|
2773
|
+
allowIssueCacheMinutes: 0,
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2777
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
2778
|
+
env: {
|
|
2779
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-ok',
|
|
2780
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
2781
|
+
},
|
|
2782
|
+
});
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
it('should not inject env when every available token is blocked', async () => {
|
|
2786
|
+
const awaitingIssue = createMockIssue({
|
|
2787
|
+
url: 'url1',
|
|
2788
|
+
title: 'Issue 1',
|
|
2789
|
+
labels: ['category:impl'],
|
|
2790
|
+
status: 'Awaiting Workspace',
|
|
2791
|
+
number: 1,
|
|
2792
|
+
itemId: 'item-1',
|
|
2793
|
+
});
|
|
2794
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2795
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2796
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
2797
|
+
);
|
|
2798
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2799
|
+
stdout: '',
|
|
2800
|
+
stderr: '',
|
|
2801
|
+
exitCode: 0,
|
|
2802
|
+
});
|
|
2803
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2804
|
+
{ token: 'token-a', fiveHourUtilization: 5, blocked: true },
|
|
2805
|
+
{ token: 'token-b', fiveHourUtilization: 8, blocked: true },
|
|
2806
|
+
]);
|
|
2807
|
+
|
|
2808
|
+
await useCase.run({
|
|
2809
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2810
|
+
defaultAgentName: 'agent1',
|
|
2811
|
+
defaultLlmModelName: 'claude-opus',
|
|
2812
|
+
defaultLlmAgentName: null,
|
|
2813
|
+
configFilePath: '/path/to/config.yml',
|
|
2814
|
+
maximumPreparingIssuesCount: null,
|
|
2815
|
+
utilizationPercentageThreshold: 90,
|
|
2816
|
+
allowedIssueAuthors: null,
|
|
2817
|
+
codexHomeCandidates: null,
|
|
2818
|
+
allowIssueCacheMinutes: 0,
|
|
2819
|
+
});
|
|
2820
|
+
|
|
2821
|
+
expect(
|
|
2822
|
+
mockClaudeTokenUsageRepository.ensureObservable.mock.calls,
|
|
2823
|
+
).toHaveLength(0);
|
|
2824
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2825
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toBeUndefined();
|
|
2826
|
+
});
|
|
2519
2827
|
});
|
|
@@ -2,6 +2,8 @@ import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
|
2
2
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
3
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
4
4
|
import { ClaudeRepository } from './adapter-interfaces/ClaudeRepository';
|
|
5
|
+
import { ClaudeTokenUsageRepository } from './adapter-interfaces/ClaudeTokenUsageRepository';
|
|
6
|
+
import { ClaudeTokenUsage } from '../entities/ClaudeTokenUsage';
|
|
5
7
|
import {
|
|
6
8
|
AWAITING_WORKSPACE_STATUS_NAME,
|
|
7
9
|
PREPARATION_STATUS_NAME,
|
|
@@ -22,8 +24,15 @@ export class StartPreparationUseCase {
|
|
|
22
24
|
>,
|
|
23
25
|
private readonly claudeRepository: Pick<ClaudeRepository, 'getUsage'>,
|
|
24
26
|
private readonly localCommandRunner: LocalCommandRunner,
|
|
27
|
+
private readonly claudeTokenUsageRepository: ClaudeTokenUsageRepository,
|
|
25
28
|
) {}
|
|
26
29
|
|
|
30
|
+
private selectRotationTokens = (tokenUsages: ClaudeTokenUsage[]): string[] =>
|
|
31
|
+
tokenUsages
|
|
32
|
+
.filter((usage) => !usage.blocked)
|
|
33
|
+
.sort((a, b) => a.fiveHourUtilization - b.fiveHourUtilization)
|
|
34
|
+
.map((usage) => usage.token);
|
|
35
|
+
|
|
27
36
|
run = async (params: {
|
|
28
37
|
projectUrl: string;
|
|
29
38
|
defaultAgentName: string;
|
|
@@ -84,6 +93,20 @@ export class StartPreparationUseCase {
|
|
|
84
93
|
);
|
|
85
94
|
}
|
|
86
95
|
}
|
|
96
|
+
|
|
97
|
+
const tokenUsages =
|
|
98
|
+
await this.claudeTokenUsageRepository.getAvailableTokenUsages();
|
|
99
|
+
let rotationTokens: string[] | null = null;
|
|
100
|
+
let proxyBaseUrl: string | null = null;
|
|
101
|
+
if (tokenUsages.length > 0) {
|
|
102
|
+
const ranked = this.selectRotationTokens(tokenUsages);
|
|
103
|
+
if (ranked.length > 0) {
|
|
104
|
+
await this.claudeTokenUsageRepository.ensureObservable();
|
|
105
|
+
rotationTokens = ranked;
|
|
106
|
+
proxyBaseUrl = this.claudeTokenUsageRepository.proxyBaseUrl();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
88
111
|
const storyObjectMap = await this.issueRepository.getStoryObjectMap(
|
|
89
112
|
project,
|
|
@@ -271,7 +294,20 @@ export class StartPreparationUseCase {
|
|
|
271
294
|
];
|
|
272
295
|
awArgs.push('--codexHome', codexHome);
|
|
273
296
|
}
|
|
274
|
-
|
|
297
|
+
let spawnEnv: Record<string, string> | undefined;
|
|
298
|
+
if (rotationTokens !== null && proxyBaseUrl !== null) {
|
|
299
|
+
const selected =
|
|
300
|
+
rotationTokens[startedInThisRunCount % rotationTokens.length];
|
|
301
|
+
spawnEnv = {
|
|
302
|
+
CLAUDE_CODE_OAUTH_TOKEN: selected,
|
|
303
|
+
ANTHROPIC_BASE_URL: proxyBaseUrl,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
await this.localCommandRunner.runCommand(
|
|
307
|
+
'aw',
|
|
308
|
+
awArgs,
|
|
309
|
+
spawnEnv ? { env: spawnEnv } : undefined,
|
|
310
|
+
);
|
|
275
311
|
startedInThisRunCount++;
|
|
276
312
|
updatedCurrentPreparationIssueCount++;
|
|
277
313
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
export interface LocalCommandRunnerOptions {
|
|
2
|
+
env?: Record<string, string>;
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export interface LocalCommandRunner {
|
|
2
6
|
runCommand(
|
|
3
7
|
program: string,
|
|
4
8
|
args: string[],
|
|
9
|
+
options?: LocalCommandRunnerOptions,
|
|
5
10
|
): Promise<{
|
|
6
11
|
stdout: string;
|
|
7
12
|
stderr: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAkEzB,eAAO,MAAM,OAAO,SAAgB,CAAC"}
|