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.
Files changed (52) hide show
  1. package/.github/workflows/umino-project.yml +5 -4
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +27 -9
  4. package/bin/adapter/entry-points/cli/index.js +68 -10
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +44 -8
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  8. package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
  9. package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
  10. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +412 -177
  11. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js +6 -2
  13. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  14. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
  15. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  16. package/bin/domain/usecases/StartPreparationUseCase.js +115 -72
  17. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/adapter/entry-points/cli/index.test.ts +184 -13
  20. package/src/adapter/entry-points/cli/index.ts +105 -13
  21. package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
  22. package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
  23. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
  24. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +626 -265
  25. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
  26. package/src/domain/entities/Issue.ts +1 -0
  27. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  28. package/src/domain/usecases/HandleScheduledEventUseCase.ts +13 -3
  29. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
  30. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +64 -9
  31. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
  32. package/src/domain/usecases/StartPreparationUseCase.test.ts +1978 -295
  33. package/src/domain/usecases/StartPreparationUseCase.ts +185 -126
  34. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -1
  35. package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
  36. package/types/adapter/entry-points/cli/index.d.ts +5 -1
  37. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  38. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
  39. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
  40. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +5 -3
  41. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  42. package/types/domain/entities/Issue.d.ts +1 -0
  43. package/types/domain/entities/Issue.d.ts.map +1 -1
  44. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
  45. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  46. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  47. package/types/domain/usecases/StartPreparationUseCase.d.ts +11 -18
  48. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  49. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -1
  50. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  51. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
  52. 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
- logFilePath: '/path/to/log.txt',
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
- thresholdForAutoReject: 'ghi',
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
- allowIssueCacheMinutes: 10
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.allowIssueCacheMinutes).toBe(10);
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
- logFilePath: undefined,
509
+ defaultLlmModelName: null,
510
+ defaultLlmAgentName: null,
511
+ configFilePath: configFilePath,
450
512
  maximumPreparingIssuesCount: null,
451
- allowIssueCacheMinutes: 0,
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
- logFilePath: undefined,
550
+ defaultLlmModelName: null,
551
+ defaultLlmAgentName: null,
552
+ configFilePath: configFilePath,
487
553
  maximumPreparingIssuesCount: null,
488
- allowIssueCacheMinutes: 0,
554
+ utilizationPercentageThreshold: 90,
555
+ allowedIssueAuthors: null,
556
+ codexHomeCandidates: null,
489
557
  });
490
558
  });
491
559
 
492
- it('should pass logFilePath from config file', async () => {
493
- const configWithLog = {
560
+ it('should pass defaultLlmModelName and allowedIssueAuthors from config file', async () => {
561
+ const configWithLlm = {
494
562
  ...defaultConfig,
495
- logFilePath: '/path/to/log.txt',
563
+ defaultLlmModelName: 'claude-opus-4-5',
564
+ allowedIssueAuthors: 'user1,user2',
496
565
  };
497
- writeConfig(configWithLog);
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
- logFilePath: '/path/to/log.txt',
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
- logFilePath?: string;
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
- logFilePath?: string;
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
- logFilePath: getStringValue(parsed, 'logFilePath'),
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
- logFilePath: getStringValue(parsed, 'logFilePath'),
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
- logFilePath:
194
- readmeOverrides.logFilePath ??
195
- cliOverrides.logFilePath ??
196
- configFile.logFilePath,
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('--logFilePath <path>', 'Path to log file')
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
- logFilePath: options.logFilePath,
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
- logFilePath: logFilePath ?? undefined,
623
+ defaultLlmModelName: config.defaultLlmModelName ?? null,
624
+ defaultLlmAgentName: config.defaultLlmAgentName ?? null,
625
+ configFilePath: options.configFilePath,
537
626
  maximumPreparingIssuesCount,
538
- allowIssueCacheMinutes,
627
+ utilizationPercentageThreshold:
628
+ config.utilizationPercentageThreshold ?? 90,
629
+ allowedIssueAuthors,
630
+ codexHomeCandidates,
539
631
  });
540
632
  });
541
633
 
@@ -1,11 +1,11 @@
1
- const mockExecAsync = jest.fn();
1
+ const mockExecFileAsync = jest.fn();
2
2
 
3
3
  jest.mock('child_process', () => ({
4
- exec: jest.fn(),
4
+ execFile: jest.fn(),
5
5
  }));
6
6
 
7
7
  jest.mock('util', () => ({
8
- promisify: jest.fn(() => mockExecAsync),
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
- mockExecAsync.mockResolvedValue({
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(mockExecAsync).toHaveBeenCalledWith('echo "test"');
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
- mockExecAsync.mockRejectedValue(error);
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
- mockExecAsync.mockRejectedValue(error);
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
- mockExecAsync.mockRejectedValue(new Error('Unexpected error'));
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 { exec } from 'child_process';
2
+ import { execFile } from 'child_process';
3
3
  import { promisify } from 'util';
4
4
 
5
- const execAsync = promisify(exec);
5
+ const execFileAsync = promisify(execFile);
6
6
 
7
7
  export class NodeLocalCommandRunner implements LocalCommandRunner {
8
- async runCommand(command: string): Promise<{
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 execAsync(command);
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