github-issue-tower-defence-management 1.40.0 → 1.42.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/.github/workflows/umino-project.yml +5 -4
- package/CHANGELOG.md +20 -0
- package/README.md +27 -9
- package/bin/adapter/entry-points/cli/index.js +68 -10
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +44 -8
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
- package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +412 -177
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +6 -2
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +115 -72
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +184 -13
- package/src/adapter/entry-points/cli/index.ts +105 -13
- package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
- package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +626 -265
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +13 -3
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +64 -9
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1978 -295
- package/src/domain/usecases/StartPreparationUseCase.ts +185 -126
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -1
- package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
- package/types/adapter/entry-points/cli/index.d.ts +5 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +5 -3
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +11 -18
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
|
@@ -128,13 +128,17 @@ describe('CLI', () => {
|
|
|
128
128
|
awaitingWorkspaceStatus: 'Awaiting',
|
|
129
129
|
preparationStatus: 'Preparing',
|
|
130
130
|
defaultAgentName: 'agent1',
|
|
131
|
-
|
|
131
|
+
defaultLlmModelName: 'claude-opus-4-5',
|
|
132
|
+
defaultLlmAgentName: 'aw',
|
|
132
133
|
maximumPreparingIssuesCount: 10,
|
|
133
134
|
allowIssueCacheMinutes: 5,
|
|
135
|
+
utilizationPercentageThreshold: 80,
|
|
136
|
+
allowedIssueAuthors: 'user1,user2',
|
|
134
137
|
awaitingQualityCheckStatus: 'Awaiting QC',
|
|
135
138
|
thresholdForAutoReject: 5,
|
|
136
139
|
workflowBlockerResolvedWebhookUrl: 'https://example.com/webhook',
|
|
137
140
|
projectName: 'test-project',
|
|
141
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-main'],
|
|
138
142
|
};
|
|
139
143
|
writeConfig(config);
|
|
140
144
|
|
|
@@ -143,6 +147,42 @@ describe('CLI', () => {
|
|
|
143
147
|
expect(result).toEqual(config);
|
|
144
148
|
});
|
|
145
149
|
|
|
150
|
+
it('should load codexHomeCandidates string array from config file', () => {
|
|
151
|
+
const config = {
|
|
152
|
+
...defaultConfig,
|
|
153
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-dev2', '.codex-main'],
|
|
154
|
+
};
|
|
155
|
+
writeConfig(config);
|
|
156
|
+
|
|
157
|
+
const result = loadConfigFile(configFilePath);
|
|
158
|
+
|
|
159
|
+
expect(result.codexHomeCandidates).toEqual([
|
|
160
|
+
'.codex-dev1',
|
|
161
|
+
'.codex-dev2',
|
|
162
|
+
'.codex-main',
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return undefined codexHomeCandidates when not in config', () => {
|
|
167
|
+
writeConfig(defaultConfig);
|
|
168
|
+
|
|
169
|
+
const result = loadConfigFile(configFilePath);
|
|
170
|
+
|
|
171
|
+
expect(result.codexHomeCandidates).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return undefined codexHomeCandidates when array contains non-string items', () => {
|
|
175
|
+
const config = {
|
|
176
|
+
...defaultConfig,
|
|
177
|
+
codexHomeCandidates: ['.codex-dev1', 123],
|
|
178
|
+
};
|
|
179
|
+
writeConfig(config);
|
|
180
|
+
|
|
181
|
+
const result = loadConfigFile(configFilePath);
|
|
182
|
+
|
|
183
|
+
expect(result.codexHomeCandidates).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
146
186
|
it('should return empty config for empty YAML', () => {
|
|
147
187
|
fs.writeFileSync(configFilePath, '');
|
|
148
188
|
|
|
@@ -168,7 +208,8 @@ describe('CLI', () => {
|
|
|
168
208
|
const config = {
|
|
169
209
|
maximumPreparingIssuesCount: 'abc',
|
|
170
210
|
allowIssueCacheMinutes: 'def',
|
|
171
|
-
|
|
211
|
+
utilizationPercentageThreshold: 'ghi',
|
|
212
|
+
thresholdForAutoReject: 'jkl',
|
|
172
213
|
};
|
|
173
214
|
writeConfig(config);
|
|
174
215
|
|
|
@@ -176,6 +217,7 @@ describe('CLI', () => {
|
|
|
176
217
|
|
|
177
218
|
expect(result.maximumPreparingIssuesCount).toBeUndefined();
|
|
178
219
|
expect(result.allowIssueCacheMinutes).toBeUndefined();
|
|
220
|
+
expect(result.utilizationPercentageThreshold).toBeUndefined();
|
|
179
221
|
expect(result.thresholdForAutoReject).toBeUndefined();
|
|
180
222
|
});
|
|
181
223
|
|
|
@@ -274,14 +316,14 @@ defaultAgentName: 'readme-agent'
|
|
|
274
316
|
const readme = `<details>
|
|
275
317
|
<summary>config</summary>
|
|
276
318
|
maximumPreparingIssuesCount: 15
|
|
277
|
-
|
|
319
|
+
utilizationPercentageThreshold: 80
|
|
278
320
|
thresholdForAutoReject: 5
|
|
279
321
|
</details>`;
|
|
280
322
|
|
|
281
323
|
const result = parseProjectReadmeConfig(readme);
|
|
282
324
|
|
|
283
325
|
expect(result.maximumPreparingIssuesCount).toBe(15);
|
|
284
|
-
expect(result.
|
|
326
|
+
expect(result.utilizationPercentageThreshold).toBe(80);
|
|
285
327
|
expect(result.thresholdForAutoReject).toBe(5);
|
|
286
328
|
});
|
|
287
329
|
|
|
@@ -295,6 +337,24 @@ defaultAgentName: 'case-test-agent'
|
|
|
295
337
|
|
|
296
338
|
expect(result.defaultAgentName).toBe('case-test-agent');
|
|
297
339
|
});
|
|
340
|
+
|
|
341
|
+
it('should parse codexHomeCandidates string array from README config', () => {
|
|
342
|
+
const readme = `<details>
|
|
343
|
+
<summary>config</summary>
|
|
344
|
+
codexHomeCandidates:
|
|
345
|
+
- .codex-dev1
|
|
346
|
+
- .codex-dev2
|
|
347
|
+
- .codex-main
|
|
348
|
+
</details>`;
|
|
349
|
+
|
|
350
|
+
const result = parseProjectReadmeConfig(readme);
|
|
351
|
+
|
|
352
|
+
expect(result.codexHomeCandidates).toEqual([
|
|
353
|
+
'.codex-dev1',
|
|
354
|
+
'.codex-dev2',
|
|
355
|
+
'.codex-main',
|
|
356
|
+
]);
|
|
357
|
+
});
|
|
298
358
|
});
|
|
299
359
|
|
|
300
360
|
describe('mergeConfigs', () => {
|
|
@@ -446,9 +506,13 @@ defaultAgentName: 'case-test-agent'
|
|
|
446
506
|
awaitingWorkspaceStatus: 'Awaiting',
|
|
447
507
|
preparationStatus: 'Preparing',
|
|
448
508
|
defaultAgentName: 'agent1',
|
|
449
|
-
|
|
509
|
+
defaultLlmModelName: null,
|
|
510
|
+
defaultLlmAgentName: null,
|
|
511
|
+
configFilePath: configFilePath,
|
|
450
512
|
maximumPreparingIssuesCount: null,
|
|
451
|
-
|
|
513
|
+
utilizationPercentageThreshold: 90,
|
|
514
|
+
allowedIssueAuthors: null,
|
|
515
|
+
codexHomeCandidates: null,
|
|
452
516
|
});
|
|
453
517
|
});
|
|
454
518
|
|
|
@@ -483,18 +547,23 @@ defaultAgentName: 'case-test-agent'
|
|
|
483
547
|
awaitingWorkspaceStatus: 'Awaiting',
|
|
484
548
|
preparationStatus: 'Preparing',
|
|
485
549
|
defaultAgentName: 'override-agent',
|
|
486
|
-
|
|
550
|
+
defaultLlmModelName: null,
|
|
551
|
+
defaultLlmAgentName: null,
|
|
552
|
+
configFilePath: configFilePath,
|
|
487
553
|
maximumPreparingIssuesCount: null,
|
|
488
|
-
|
|
554
|
+
utilizationPercentageThreshold: 90,
|
|
555
|
+
allowedIssueAuthors: null,
|
|
556
|
+
codexHomeCandidates: null,
|
|
489
557
|
});
|
|
490
558
|
});
|
|
491
559
|
|
|
492
|
-
it('should pass
|
|
493
|
-
const
|
|
560
|
+
it('should pass defaultLlmModelName and allowedIssueAuthors from config file', async () => {
|
|
561
|
+
const configWithLlm = {
|
|
494
562
|
...defaultConfig,
|
|
495
|
-
|
|
563
|
+
defaultLlmModelName: 'claude-opus-4-5',
|
|
564
|
+
allowedIssueAuthors: 'user1,user2',
|
|
496
565
|
};
|
|
497
|
-
writeConfig(
|
|
566
|
+
writeConfig(configWithLlm);
|
|
498
567
|
|
|
499
568
|
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
500
569
|
const MockedStartPreparationUseCase = jest.mocked(
|
|
@@ -518,7 +587,8 @@ defaultAgentName: 'case-test-agent'
|
|
|
518
587
|
|
|
519
588
|
expect(mockRun).toHaveBeenCalledWith(
|
|
520
589
|
expect.objectContaining({
|
|
521
|
-
|
|
590
|
+
defaultLlmModelName: 'claude-opus-4-5',
|
|
591
|
+
allowedIssueAuthors: ['user1', 'user2'],
|
|
522
592
|
}),
|
|
523
593
|
);
|
|
524
594
|
});
|
|
@@ -892,6 +962,107 @@ defaultAgentName: 'case-test-agent'
|
|
|
892
962
|
}),
|
|
893
963
|
);
|
|
894
964
|
});
|
|
965
|
+
|
|
966
|
+
it('should pass codexHomeCandidates from config file', async () => {
|
|
967
|
+
const configWithCandidates = {
|
|
968
|
+
...defaultConfig,
|
|
969
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-dev2', '.codex-main'],
|
|
970
|
+
};
|
|
971
|
+
writeConfig(configWithCandidates);
|
|
972
|
+
|
|
973
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
974
|
+
const MockedStartPreparationUseCase = jest.mocked(
|
|
975
|
+
StartPreparationUseCase,
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
MockedStartPreparationUseCase.mockImplementation(function (
|
|
979
|
+
this: StartPreparationUseCase,
|
|
980
|
+
) {
|
|
981
|
+
this.run = mockRun;
|
|
982
|
+
return this;
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
await program.parseAsync([
|
|
986
|
+
'node',
|
|
987
|
+
'test',
|
|
988
|
+
'startDaemon',
|
|
989
|
+
'--configFilePath',
|
|
990
|
+
configFilePath,
|
|
991
|
+
]);
|
|
992
|
+
|
|
993
|
+
expect(mockRun).toHaveBeenCalledWith(
|
|
994
|
+
expect.objectContaining({
|
|
995
|
+
codexHomeCandidates: ['.codex-dev1', '.codex-dev2', '.codex-main'],
|
|
996
|
+
}),
|
|
997
|
+
);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should pass codexHomeCandidates as null when not in config', async () => {
|
|
1001
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
1002
|
+
const MockedStartPreparationUseCase = jest.mocked(
|
|
1003
|
+
StartPreparationUseCase,
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
MockedStartPreparationUseCase.mockImplementation(function (
|
|
1007
|
+
this: StartPreparationUseCase,
|
|
1008
|
+
) {
|
|
1009
|
+
this.run = mockRun;
|
|
1010
|
+
return this;
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
await program.parseAsync([
|
|
1014
|
+
'node',
|
|
1015
|
+
'test',
|
|
1016
|
+
'startDaemon',
|
|
1017
|
+
'--configFilePath',
|
|
1018
|
+
configFilePath,
|
|
1019
|
+
]);
|
|
1020
|
+
|
|
1021
|
+
expect(mockRun).toHaveBeenCalledWith(
|
|
1022
|
+
expect.objectContaining({
|
|
1023
|
+
codexHomeCandidates: null,
|
|
1024
|
+
}),
|
|
1025
|
+
);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it('should pass codexHomeCandidates from README config', async () => {
|
|
1029
|
+
const readmeContent = [
|
|
1030
|
+
'# Project',
|
|
1031
|
+
'<details>',
|
|
1032
|
+
'<summary>config</summary>',
|
|
1033
|
+
'codexHomeCandidates:',
|
|
1034
|
+
' - .codex-readme1',
|
|
1035
|
+
' - .codex-readme2',
|
|
1036
|
+
'</details>',
|
|
1037
|
+
].join('\n');
|
|
1038
|
+
mockFetchReturningReadme(readmeContent);
|
|
1039
|
+
|
|
1040
|
+
const mockRun = jest.fn().mockResolvedValue(undefined);
|
|
1041
|
+
const MockedStartPreparationUseCase = jest.mocked(
|
|
1042
|
+
StartPreparationUseCase,
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
MockedStartPreparationUseCase.mockImplementation(function (
|
|
1046
|
+
this: StartPreparationUseCase,
|
|
1047
|
+
) {
|
|
1048
|
+
this.run = mockRun;
|
|
1049
|
+
return this;
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
await program.parseAsync([
|
|
1053
|
+
'node',
|
|
1054
|
+
'test',
|
|
1055
|
+
'startDaemon',
|
|
1056
|
+
'--configFilePath',
|
|
1057
|
+
configFilePath,
|
|
1058
|
+
]);
|
|
1059
|
+
|
|
1060
|
+
expect(mockRun).toHaveBeenCalledWith(
|
|
1061
|
+
expect.objectContaining({
|
|
1062
|
+
codexHomeCandidates: ['.codex-readme1', '.codex-readme2'],
|
|
1063
|
+
}),
|
|
1064
|
+
);
|
|
1065
|
+
});
|
|
895
1066
|
});
|
|
896
1067
|
|
|
897
1068
|
describe('notifyFinishedIssuePreparation', () => {
|
|
@@ -26,14 +26,18 @@ type ConfigFile = {
|
|
|
26
26
|
awaitingWorkspaceStatus?: string;
|
|
27
27
|
preparationStatus?: string;
|
|
28
28
|
defaultAgentName?: string;
|
|
29
|
-
|
|
29
|
+
defaultLlmModelName?: string;
|
|
30
|
+
defaultLlmAgentName?: string;
|
|
30
31
|
maximumPreparingIssuesCount?: number;
|
|
31
32
|
allowIssueCacheMinutes?: number;
|
|
33
|
+
utilizationPercentageThreshold?: number;
|
|
34
|
+
allowedIssueAuthors?: string;
|
|
32
35
|
awaitingQualityCheckStatus?: string;
|
|
33
36
|
thresholdForAutoReject?: number;
|
|
34
37
|
workflowBlockerResolvedWebhookUrl?: string;
|
|
35
38
|
projectName?: string;
|
|
36
39
|
preparationProcessCheckCommand?: string;
|
|
40
|
+
codexHomeCandidates?: string[];
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
type StartDaemonOptions = {
|
|
@@ -41,9 +45,12 @@ type StartDaemonOptions = {
|
|
|
41
45
|
awaitingWorkspaceStatus?: string;
|
|
42
46
|
preparationStatus?: string;
|
|
43
47
|
defaultAgentName?: string;
|
|
44
|
-
|
|
48
|
+
defaultLlmModelName?: string;
|
|
49
|
+
defaultLlmAgentName?: string;
|
|
45
50
|
maximumPreparingIssuesCount?: string;
|
|
46
51
|
allowIssueCacheMinutes?: string;
|
|
52
|
+
utilizationPercentageThreshold?: string;
|
|
53
|
+
allowedIssueAuthors?: string;
|
|
47
54
|
preparationProcessCheckCommand?: string;
|
|
48
55
|
configFilePath: string;
|
|
49
56
|
};
|
|
@@ -75,6 +82,24 @@ const getNumberValue = (
|
|
|
75
82
|
return typeof value === 'number' ? value : undefined;
|
|
76
83
|
};
|
|
77
84
|
|
|
85
|
+
const getStringArrayValue = (
|
|
86
|
+
obj: Record<string, unknown>,
|
|
87
|
+
key: string,
|
|
88
|
+
): string[] | undefined => {
|
|
89
|
+
const value = obj[key];
|
|
90
|
+
if (!Array.isArray(value)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const strings: string[] = [];
|
|
94
|
+
for (const item of value) {
|
|
95
|
+
if (typeof item !== 'string') {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
strings.push(item);
|
|
99
|
+
}
|
|
100
|
+
return strings;
|
|
101
|
+
};
|
|
102
|
+
|
|
78
103
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
79
104
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
80
105
|
|
|
@@ -93,12 +118,18 @@ export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
|
93
118
|
),
|
|
94
119
|
preparationStatus: getStringValue(parsed, 'preparationStatus'),
|
|
95
120
|
defaultAgentName: getStringValue(parsed, 'defaultAgentName'),
|
|
96
|
-
|
|
121
|
+
defaultLlmModelName: getStringValue(parsed, 'defaultLlmModelName'),
|
|
122
|
+
defaultLlmAgentName: getStringValue(parsed, 'defaultLlmAgentName'),
|
|
97
123
|
maximumPreparingIssuesCount: getNumberValue(
|
|
98
124
|
parsed,
|
|
99
125
|
'maximumPreparingIssuesCount',
|
|
100
126
|
),
|
|
101
127
|
allowIssueCacheMinutes: getNumberValue(parsed, 'allowIssueCacheMinutes'),
|
|
128
|
+
utilizationPercentageThreshold: getNumberValue(
|
|
129
|
+
parsed,
|
|
130
|
+
'utilizationPercentageThreshold',
|
|
131
|
+
),
|
|
132
|
+
allowedIssueAuthors: getStringValue(parsed, 'allowedIssueAuthors'),
|
|
102
133
|
awaitingQualityCheckStatus: getStringValue(
|
|
103
134
|
parsed,
|
|
104
135
|
'awaitingQualityCheckStatus',
|
|
@@ -113,6 +144,7 @@ export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
|
113
144
|
parsed,
|
|
114
145
|
'preparationProcessCheckCommand',
|
|
115
146
|
),
|
|
147
|
+
codexHomeCandidates: getStringArrayValue(parsed, 'codexHomeCandidates'),
|
|
116
148
|
};
|
|
117
149
|
} catch (error) {
|
|
118
150
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -146,12 +178,18 @@ export const parseProjectReadmeConfig = (readme: string): ConfigFile => {
|
|
|
146
178
|
),
|
|
147
179
|
preparationStatus: getStringValue(parsed, 'preparationStatus'),
|
|
148
180
|
defaultAgentName: getStringValue(parsed, 'defaultAgentName'),
|
|
149
|
-
|
|
181
|
+
defaultLlmModelName: getStringValue(parsed, 'defaultLlmModelName'),
|
|
182
|
+
defaultLlmAgentName: getStringValue(parsed, 'defaultLlmAgentName'),
|
|
150
183
|
maximumPreparingIssuesCount: getNumberValue(
|
|
151
184
|
parsed,
|
|
152
185
|
'maximumPreparingIssuesCount',
|
|
153
186
|
),
|
|
154
187
|
allowIssueCacheMinutes: getNumberValue(parsed, 'allowIssueCacheMinutes'),
|
|
188
|
+
utilizationPercentageThreshold: getNumberValue(
|
|
189
|
+
parsed,
|
|
190
|
+
'utilizationPercentageThreshold',
|
|
191
|
+
),
|
|
192
|
+
allowedIssueAuthors: getStringValue(parsed, 'allowedIssueAuthors'),
|
|
155
193
|
awaitingQualityCheckStatus: getStringValue(
|
|
156
194
|
parsed,
|
|
157
195
|
'awaitingQualityCheckStatus',
|
|
@@ -165,6 +203,7 @@ export const parseProjectReadmeConfig = (readme: string): ConfigFile => {
|
|
|
165
203
|
parsed,
|
|
166
204
|
'preparationProcessCheckCommand',
|
|
167
205
|
),
|
|
206
|
+
codexHomeCandidates: getStringArrayValue(parsed, 'codexHomeCandidates'),
|
|
168
207
|
};
|
|
169
208
|
} catch {
|
|
170
209
|
console.warn('Failed to parse YAML from project README config section');
|
|
@@ -190,10 +229,14 @@ export const mergeConfigs = (
|
|
|
190
229
|
readmeOverrides.defaultAgentName ??
|
|
191
230
|
cliOverrides.defaultAgentName ??
|
|
192
231
|
configFile.defaultAgentName,
|
|
193
|
-
|
|
194
|
-
readmeOverrides.
|
|
195
|
-
cliOverrides.
|
|
196
|
-
configFile.
|
|
232
|
+
defaultLlmModelName:
|
|
233
|
+
readmeOverrides.defaultLlmModelName ??
|
|
234
|
+
cliOverrides.defaultLlmModelName ??
|
|
235
|
+
configFile.defaultLlmModelName,
|
|
236
|
+
defaultLlmAgentName:
|
|
237
|
+
readmeOverrides.defaultLlmAgentName ??
|
|
238
|
+
cliOverrides.defaultLlmAgentName ??
|
|
239
|
+
configFile.defaultLlmAgentName,
|
|
197
240
|
maximumPreparingIssuesCount:
|
|
198
241
|
readmeOverrides.maximumPreparingIssuesCount ??
|
|
199
242
|
cliOverrides.maximumPreparingIssuesCount ??
|
|
@@ -202,6 +245,14 @@ export const mergeConfigs = (
|
|
|
202
245
|
readmeOverrides.allowIssueCacheMinutes ??
|
|
203
246
|
cliOverrides.allowIssueCacheMinutes ??
|
|
204
247
|
configFile.allowIssueCacheMinutes,
|
|
248
|
+
utilizationPercentageThreshold:
|
|
249
|
+
readmeOverrides.utilizationPercentageThreshold ??
|
|
250
|
+
cliOverrides.utilizationPercentageThreshold ??
|
|
251
|
+
configFile.utilizationPercentageThreshold,
|
|
252
|
+
allowedIssueAuthors:
|
|
253
|
+
readmeOverrides.allowedIssueAuthors ??
|
|
254
|
+
cliOverrides.allowedIssueAuthors ??
|
|
255
|
+
configFile.allowedIssueAuthors,
|
|
205
256
|
awaitingQualityCheckStatus:
|
|
206
257
|
readmeOverrides.awaitingQualityCheckStatus ??
|
|
207
258
|
cliOverrides.awaitingQualityCheckStatus ??
|
|
@@ -219,6 +270,10 @@ export const mergeConfigs = (
|
|
|
219
270
|
readmeOverrides.preparationProcessCheckCommand ??
|
|
220
271
|
cliOverrides.preparationProcessCheckCommand ??
|
|
221
272
|
configFile.preparationProcessCheckCommand,
|
|
273
|
+
codexHomeCandidates:
|
|
274
|
+
readmeOverrides.codexHomeCandidates ??
|
|
275
|
+
cliOverrides.codexHomeCandidates ??
|
|
276
|
+
configFile.codexHomeCandidates,
|
|
222
277
|
});
|
|
223
278
|
|
|
224
279
|
type GraphqlProjectV2ReadmeResponse = {
|
|
@@ -361,7 +416,8 @@ program
|
|
|
361
416
|
)
|
|
362
417
|
.option('--preparationStatus <status>', 'Status for issues in preparation')
|
|
363
418
|
.option('--defaultAgentName <name>', 'Default agent name')
|
|
364
|
-
.option('--
|
|
419
|
+
.option('--defaultLlmModelName <name>', 'Default LLM model name')
|
|
420
|
+
.option('--defaultLlmAgentName <name>', 'Default LLM agent name')
|
|
365
421
|
.option(
|
|
366
422
|
'--maximumPreparingIssuesCount <count>',
|
|
367
423
|
'Maximum number of issues in preparation status (default: 6)',
|
|
@@ -370,6 +426,14 @@ program
|
|
|
370
426
|
'--allowIssueCacheMinutes <minutes>',
|
|
371
427
|
'Allow cache for issues in minutes (default: 0)',
|
|
372
428
|
)
|
|
429
|
+
.option(
|
|
430
|
+
'--utilizationPercentageThreshold <percent>',
|
|
431
|
+
'Claude utilization percentage threshold (default: 90)',
|
|
432
|
+
)
|
|
433
|
+
.option(
|
|
434
|
+
'--allowedIssueAuthors <authors>',
|
|
435
|
+
'Comma-separated list of allowed issue authors',
|
|
436
|
+
)
|
|
373
437
|
.option(
|
|
374
438
|
'--preparationProcessCheckCommand <template>',
|
|
375
439
|
'Shell command template with {URL} placeholder to check if a preparation process is alive',
|
|
@@ -388,13 +452,18 @@ program
|
|
|
388
452
|
awaitingWorkspaceStatus: options.awaitingWorkspaceStatus,
|
|
389
453
|
preparationStatus: options.preparationStatus,
|
|
390
454
|
defaultAgentName: options.defaultAgentName,
|
|
391
|
-
|
|
455
|
+
defaultLlmModelName: options.defaultLlmModelName,
|
|
456
|
+
defaultLlmAgentName: options.defaultLlmAgentName,
|
|
392
457
|
maximumPreparingIssuesCount: options.maximumPreparingIssuesCount
|
|
393
458
|
? Number(options.maximumPreparingIssuesCount)
|
|
394
459
|
: undefined,
|
|
395
460
|
allowIssueCacheMinutes: options.allowIssueCacheMinutes
|
|
396
461
|
? Number(options.allowIssueCacheMinutes)
|
|
397
462
|
: undefined,
|
|
463
|
+
utilizationPercentageThreshold: options.utilizationPercentageThreshold
|
|
464
|
+
? Number(options.utilizationPercentageThreshold)
|
|
465
|
+
: undefined,
|
|
466
|
+
allowedIssueAuthors: options.allowedIssueAuthors,
|
|
398
467
|
preparationProcessCheckCommand: options.preparationProcessCheckCommand,
|
|
399
468
|
};
|
|
400
469
|
|
|
@@ -419,7 +488,6 @@ program
|
|
|
419
488
|
const awaitingWorkspaceStatus = config.awaitingWorkspaceStatus;
|
|
420
489
|
const preparationStatus = config.preparationStatus;
|
|
421
490
|
const defaultAgentName = config.defaultAgentName;
|
|
422
|
-
const logFilePath = config.logFilePath;
|
|
423
491
|
|
|
424
492
|
if (!projectUrl) {
|
|
425
493
|
console.error(
|
|
@@ -484,6 +552,12 @@ program
|
|
|
484
552
|
const projectRepository = {
|
|
485
553
|
...new GraphqlProjectRepository(...githubRepositoryParams),
|
|
486
554
|
...new CheerioProjectRepository(...githubRepositoryParams),
|
|
555
|
+
prepareStatus: async (
|
|
556
|
+
_name: string,
|
|
557
|
+
project: Project,
|
|
558
|
+
): Promise<Project> => {
|
|
559
|
+
return project;
|
|
560
|
+
},
|
|
487
561
|
};
|
|
488
562
|
const apiV3IssueRepository = new ApiV3IssueRepository(
|
|
489
563
|
...githubRepositoryParams,
|
|
@@ -528,14 +602,32 @@ program
|
|
|
528
602
|
localCommandRunner,
|
|
529
603
|
);
|
|
530
604
|
|
|
605
|
+
const rawAllowedIssueAuthors = config.allowedIssueAuthors;
|
|
606
|
+
const allowedIssueAuthors = rawAllowedIssueAuthors
|
|
607
|
+
? rawAllowedIssueAuthors
|
|
608
|
+
.split(',')
|
|
609
|
+
.map((s) => s.trim())
|
|
610
|
+
.filter(Boolean)
|
|
611
|
+
: null;
|
|
612
|
+
|
|
613
|
+
const codexHomeCandidates =
|
|
614
|
+
config.codexHomeCandidates && config.codexHomeCandidates.length > 0
|
|
615
|
+
? config.codexHomeCandidates
|
|
616
|
+
: null;
|
|
617
|
+
|
|
531
618
|
await useCase.run({
|
|
532
619
|
projectUrl,
|
|
533
620
|
awaitingWorkspaceStatus,
|
|
534
621
|
preparationStatus,
|
|
535
622
|
defaultAgentName,
|
|
536
|
-
|
|
623
|
+
defaultLlmModelName: config.defaultLlmModelName ?? null,
|
|
624
|
+
defaultLlmAgentName: config.defaultLlmAgentName ?? null,
|
|
625
|
+
configFilePath: options.configFilePath,
|
|
537
626
|
maximumPreparingIssuesCount,
|
|
538
|
-
|
|
627
|
+
utilizationPercentageThreshold:
|
|
628
|
+
config.utilizationPercentageThreshold ?? 90,
|
|
629
|
+
allowedIssueAuthors,
|
|
630
|
+
codexHomeCandidates,
|
|
539
631
|
});
|
|
540
632
|
});
|
|
541
633
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const
|
|
1
|
+
const mockExecFileAsync = jest.fn();
|
|
2
2
|
|
|
3
3
|
jest.mock('child_process', () => ({
|
|
4
|
-
|
|
4
|
+
execFile: jest.fn(),
|
|
5
5
|
}));
|
|
6
6
|
|
|
7
7
|
jest.mock('util', () => ({
|
|
8
|
-
promisify: jest.fn(() =>
|
|
8
|
+
promisify: jest.fn(() => mockExecFileAsync),
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
11
|
import { NodeLocalCommandRunner } from './NodeLocalCommandRunner';
|
|
@@ -20,19 +20,19 @@ describe('NodeLocalCommandRunner', () => {
|
|
|
20
20
|
|
|
21
21
|
describe('runCommand', () => {
|
|
22
22
|
it('should execute command successfully', async () => {
|
|
23
|
-
|
|
23
|
+
mockExecFileAsync.mockResolvedValue({
|
|
24
24
|
stdout: 'command output',
|
|
25
25
|
stderr: '',
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
const result = await runner.runCommand('echo "test"');
|
|
28
|
+
const result = await runner.runCommand('echo', ['"test"']);
|
|
29
29
|
|
|
30
30
|
expect(result).toEqual({
|
|
31
31
|
stdout: 'command output',
|
|
32
32
|
stderr: '',
|
|
33
33
|
exitCode: 0,
|
|
34
34
|
});
|
|
35
|
-
expect(
|
|
35
|
+
expect(mockExecFileAsync).toHaveBeenCalledWith('echo', ['"test"']);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it('should handle command errors with exit code', async () => {
|
|
@@ -41,9 +41,9 @@ describe('NodeLocalCommandRunner', () => {
|
|
|
41
41
|
stdout: 'partial output',
|
|
42
42
|
stderr: 'error message',
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
mockExecFileAsync.mockRejectedValue(error);
|
|
45
45
|
|
|
46
|
-
const result = await runner.runCommand('invalid-command');
|
|
46
|
+
const result = await runner.runCommand('invalid-command', []);
|
|
47
47
|
|
|
48
48
|
expect(result).toEqual({
|
|
49
49
|
stdout: 'partial output',
|
|
@@ -58,9 +58,9 @@ describe('NodeLocalCommandRunner', () => {
|
|
|
58
58
|
stdout: '',
|
|
59
59
|
stderr: 'command not found',
|
|
60
60
|
});
|
|
61
|
-
|
|
61
|
+
mockExecFileAsync.mockRejectedValue(error);
|
|
62
62
|
|
|
63
|
-
const result = await runner.runCommand('nonexistent');
|
|
63
|
+
const result = await runner.runCommand('nonexistent', []);
|
|
64
64
|
|
|
65
65
|
expect(result).toEqual({
|
|
66
66
|
stdout: '',
|
|
@@ -70,9 +70,9 @@ describe('NodeLocalCommandRunner', () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it('should throw error if error format is unexpected', async () => {
|
|
73
|
-
|
|
73
|
+
mockExecFileAsync.mockRejectedValue(new Error('Unexpected error'));
|
|
74
74
|
|
|
75
|
-
await expect(runner.runCommand('command')).rejects.toThrow(
|
|
75
|
+
await expect(runner.runCommand('command', [])).rejects.toThrow(
|
|
76
76
|
'Unexpected error',
|
|
77
77
|
);
|
|
78
78
|
});
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
3
|
import { promisify } from 'util';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
6
|
|
|
7
7
|
export class NodeLocalCommandRunner implements LocalCommandRunner {
|
|
8
|
-
async runCommand(
|
|
8
|
+
async runCommand(
|
|
9
|
+
program: string,
|
|
10
|
+
args: string[],
|
|
11
|
+
): Promise<{
|
|
9
12
|
stdout: string;
|
|
10
13
|
stderr: string;
|
|
11
14
|
exitCode: number;
|
|
12
15
|
}> {
|
|
13
16
|
try {
|
|
14
|
-
const { stdout, stderr } = await
|
|
17
|
+
const { stdout, stderr } = await execFileAsync(program, args);
|
|
15
18
|
return {
|
|
16
19
|
stdout,
|
|
17
20
|
stderr,
|
|
@@ -63,6 +63,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
63
63
|
isInProgress: false,
|
|
64
64
|
isClosed: false,
|
|
65
65
|
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
66
|
+
author: '',
|
|
66
67
|
},
|
|
67
68
|
},
|
|
68
69
|
{
|
|
@@ -114,6 +115,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
114
115
|
isInProgress: false,
|
|
115
116
|
isClosed: false,
|
|
116
117
|
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
118
|
+
author: '',
|
|
117
119
|
},
|
|
118
120
|
},
|
|
119
121
|
];
|
|
@@ -244,6 +246,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
244
246
|
isInProgress: false,
|
|
245
247
|
isClosed: false,
|
|
246
248
|
createdAt: new Date('2024-01-01'),
|
|
249
|
+
author: '',
|
|
247
250
|
};
|
|
248
251
|
const statusId = 'new-status-id';
|
|
249
252
|
|