github-issue-tower-defence-management 1.164.0 → 1.165.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 (106) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/adapter/entry-points/cli/index.js +1 -0
  3. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  4. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  5. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  6. package/bin/adapter/entry-points/console/consoleOperationApi.js +1 -0
  7. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  8. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
  9. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  10. package/bin/adapter/repositories/GraphqlProjectRepository.js +37 -0
  11. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  12. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +56 -5
  13. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  14. package/bin/adapter/repositories/projectFieldDefinition.js +8 -0
  15. package/bin/adapter/repositories/projectFieldDefinition.js.map +1 -1
  16. package/bin/domain/entities/RequiredProjectField.js +7 -1
  17. package/bin/domain/entities/RequiredProjectField.js.map +1 -1
  18. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  19. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  20. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +65 -44
  21. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  22. package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +28 -2
  23. package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -1
  24. package/bin/domain/usecases/StartPreparationUseCase.js +5 -4
  25. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/adapter/entry-points/cli/index.test.ts +2 -0
  28. package/src/adapter/entry-points/cli/index.ts +1 -0
  29. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  30. package/src/adapter/entry-points/console/consoleOperationApi.ts +1 -0
  31. package/src/adapter/entry-points/console/consoleTabNames.test.ts +1 -0
  32. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
  33. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +3 -0
  34. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
  35. package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
  36. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +2 -0
  37. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  38. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
  39. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
  40. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
  41. package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +2 -0
  42. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
  43. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
  44. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
  45. package/src/adapter/repositories/GraphqlProjectRepository.diskCache.test.ts +1 -0
  46. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +2 -0
  47. package/src/adapter/repositories/GraphqlProjectRepository.ts +50 -0
  48. package/src/adapter/repositories/GraphqlProjectRepositoryProjectLocation.test.ts +1 -0
  49. package/src/adapter/repositories/ProjectIssuesCacheRepository.test.ts +1 -0
  50. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +156 -0
  51. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +91 -6
  52. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
  53. package/src/adapter/repositories/projectFieldDefinition.test.ts +1 -0
  54. package/src/adapter/repositories/projectFieldDefinition.ts +12 -0
  55. package/src/domain/entities/Issue.ts +1 -0
  56. package/src/domain/entities/Project.ts +5 -0
  57. package/src/domain/entities/RequiredProjectField.ts +6 -0
  58. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
  59. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +2 -0
  60. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +1 -0
  61. package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
  62. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
  63. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +146 -0
  64. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +110 -64
  65. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
  66. package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +75 -1
  67. package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +50 -7
  68. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +2 -0
  69. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +2 -0
  70. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +2 -0
  71. package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +2 -0
  72. package/src/domain/usecases/StartPreparationUseCase.test.ts +2 -0
  73. package/src/domain/usecases/StartPreparationUseCase.ts +1 -0
  74. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
  75. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
  76. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +2 -0
  77. package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
  78. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +2 -0
  79. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  80. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  81. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  82. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  83. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  84. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  85. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +3 -0
  86. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  87. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +2 -0
  88. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  89. package/types/adapter/repositories/projectFieldDefinition.d.ts.map +1 -1
  90. package/types/domain/entities/Issue.d.ts +1 -0
  91. package/types/domain/entities/Issue.d.ts.map +1 -1
  92. package/types/domain/entities/Project.d.ts +5 -0
  93. package/types/domain/entities/Project.d.ts.map +1 -1
  94. package/types/domain/entities/RequiredProjectField.d.ts +1 -0
  95. package/types/domain/entities/RequiredProjectField.d.ts.map +1 -1
  96. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  97. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  98. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +4 -2
  99. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  100. package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts +5 -1
  101. package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -1
  102. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  103. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  104. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  105. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +3 -0
  106. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
@@ -72,6 +72,7 @@ describeWhenCredentials('GraphqlProjectRepository', () => {
72
72
  name: 'Workflow Management',
73
73
  },
74
74
  },
75
+ agent: null,
75
76
  };
76
77
 
77
78
  it('should add a new option while preserving all existing options', async () => {
@@ -151,6 +152,7 @@ describeWhenCredentials('GraphqlProjectRepository', () => {
151
152
  },
152
153
  completionDate50PercentConfidence: null,
153
154
  dependedIssueUrlSeparatedByComma: null,
155
+ agent: null,
154
156
 
155
157
  status: {
156
158
  fieldId: 'PVTSSF_lAHOAGJHa84AFhgFzgDLt0c',
@@ -601,6 +601,56 @@ export class GraphqlProjectRepository
601
601
  );
602
602
  return options;
603
603
  };
604
+ updateAgentList = async (
605
+ project: Project,
606
+ newAgentList: (Omit<FieldOption, 'id'> & {
607
+ id: FieldOption['id'] | null;
608
+ })[],
609
+ ): Promise<FieldOption[]> => {
610
+ if (!project.agent) {
611
+ throw new Error('Project has no agent field');
612
+ }
613
+ const mutation = `mutation UpdateAgentOptions($fieldId: ID!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {
614
+ updateProjectV2Field(input: {
615
+ fieldId: $fieldId
616
+ singleSelectOptions: $options
617
+ }) {
618
+ projectV2Field {
619
+ ... on ProjectV2SingleSelectField {
620
+ options {
621
+ id
622
+ name
623
+ color
624
+ description
625
+ }
626
+ }
627
+ }
628
+ }
629
+ }`;
630
+ const variables = {
631
+ fieldId: project.agent.fieldId,
632
+ options: newAgentList.map(({ id, name, color, description }) => ({
633
+ ...(id !== null ? { id } : {}),
634
+ name,
635
+ color,
636
+ description,
637
+ })),
638
+ };
639
+ const response = await postGithubGraphqlJson<{
640
+ data: {
641
+ updateProjectV2Field: {
642
+ projectV2Field: {
643
+ options: FieldOption[];
644
+ };
645
+ };
646
+ };
647
+ }>({
648
+ ghToken: this.ghToken,
649
+ query: mutation,
650
+ variables,
651
+ });
652
+ return response.data.updateProjectV2Field.projectV2Field.options;
653
+ };
604
654
  updateStatusList = async (
605
655
  project: Project,
606
656
  newStatusList: (Omit<FieldOption, 'id'> & {
@@ -186,6 +186,7 @@ describe('GraphqlProjectRepository project location', () => {
186
186
  remainingEstimationMinutes: null,
187
187
  dependedIssueUrlSeparatedByComma: null,
188
188
  completionDate50PercentConfidence: null,
189
+ agent: null,
189
190
  });
190
191
 
191
192
  expect(mockPostGithubGraphqlJson).not.toHaveBeenCalled();
@@ -55,6 +55,7 @@ const cachedProject: Project = {
55
55
  remainingEstimationMinutes: null,
56
56
  dependedIssueUrlSeparatedByComma: null,
57
57
  completionDate50PercentConfidence: null,
58
+ agent: null,
58
59
  };
59
60
 
60
61
  const savedStoryOptions: FieldOption[] = [
@@ -28,6 +28,7 @@ const buildTestProject = (id: string): Project => ({
28
28
  remainingEstimationMinutes: null,
29
29
  dependedIssueUrlSeparatedByComma: null,
30
30
  completionDate50PercentConfidence: null,
31
+ agent: null,
31
32
  });
32
33
 
33
34
  const buildCachedIssueRecord = (url: string, title: string) => ({
@@ -151,6 +152,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
151
152
  closingIssueReferenceUrls: [
152
153
  'https://github.com/HiromiShikata/test-repository/issues/7',
153
154
  ],
155
+ agent: null,
154
156
  },
155
157
  },
156
158
  {
@@ -207,6 +209,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
207
209
  createdAt: new Date('2024-01-01T00:00:00Z'),
208
210
  author: '',
209
211
  closingIssueReferenceUrls: [],
212
+ agent: null,
210
213
  },
211
214
  },
212
215
  ];
@@ -716,6 +719,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
716
719
  remainingEstimationMinutes: null,
717
720
  dependedIssueUrlSeparatedByComma: null,
718
721
  completionDate50PercentConfidence: null,
722
+ agent: null,
719
723
  };
720
724
  const issue = {
721
725
  nameWithOwner: 'HiromiShikata/test-repository',
@@ -742,6 +746,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
742
746
  createdAt: new Date('2024-01-01'),
743
747
  author: '',
744
748
  closingIssueReferenceUrls: [],
749
+ agent: null,
745
750
  };
746
751
  const statusId = 'new-status-id';
747
752
 
@@ -1171,6 +1176,157 @@ describe('ApiV3CheerioRestIssueRepository', () => {
1171
1176
  });
1172
1177
  });
1173
1178
 
1179
+ describe('mergePullRequest', () => {
1180
+ afterEach(() => {
1181
+ jest.restoreAllMocks();
1182
+ });
1183
+
1184
+ it('should PUT to the GitHub merge API with no merge_method when the repository allows merge commits', async () => {
1185
+ const fetchSpy = jest
1186
+ .spyOn(global, 'fetch')
1187
+ .mockResolvedValueOnce(new Response(null, { status: 204 }));
1188
+
1189
+ const { repository } = createApiV3CheerioRestIssueRepository();
1190
+ await repository.mergePullRequest(
1191
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1192
+ );
1193
+
1194
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
1195
+ expect(fetchSpy).toHaveBeenCalledWith(
1196
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/merge',
1197
+ expect.objectContaining({
1198
+ method: 'PUT',
1199
+ body: JSON.stringify({}),
1200
+ }),
1201
+ );
1202
+ });
1203
+
1204
+ it('should fall back to squash merge when the repository disallows merge commits (HTTP 405)', async () => {
1205
+ const fetchSpy = jest
1206
+ .spyOn(global, 'fetch')
1207
+ .mockResolvedValueOnce(
1208
+ new Response(
1209
+ JSON.stringify({
1210
+ message: 'Merge commits are not allowed on this repository.',
1211
+ }),
1212
+ { status: 405, headers: { 'Content-Type': 'application/json' } },
1213
+ ),
1214
+ )
1215
+ .mockResolvedValueOnce(
1216
+ new Response(
1217
+ JSON.stringify({
1218
+ allow_squash_merge: true,
1219
+ allow_rebase_merge: true,
1220
+ }),
1221
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1222
+ ),
1223
+ )
1224
+ .mockResolvedValueOnce(new Response(null, { status: 204 }));
1225
+
1226
+ const { repository } = createApiV3CheerioRestIssueRepository();
1227
+ await repository.mergePullRequest(
1228
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1229
+ );
1230
+
1231
+ expect(fetchSpy).toHaveBeenCalledTimes(3);
1232
+ expect(fetchSpy).toHaveBeenNthCalledWith(
1233
+ 3,
1234
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/merge',
1235
+ expect.objectContaining({
1236
+ method: 'PUT',
1237
+ body: JSON.stringify({ merge_method: 'squash' }),
1238
+ }),
1239
+ );
1240
+ });
1241
+
1242
+ it('should fall back to rebase merge when squash merge is also disallowed', async () => {
1243
+ const fetchSpy = jest
1244
+ .spyOn(global, 'fetch')
1245
+ .mockResolvedValueOnce(
1246
+ new Response(
1247
+ JSON.stringify({
1248
+ message: 'Merge commits are not allowed on this repository.',
1249
+ }),
1250
+ { status: 405, headers: { 'Content-Type': 'application/json' } },
1251
+ ),
1252
+ )
1253
+ .mockResolvedValueOnce(
1254
+ new Response(
1255
+ JSON.stringify({
1256
+ allow_squash_merge: false,
1257
+ allow_rebase_merge: true,
1258
+ }),
1259
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1260
+ ),
1261
+ )
1262
+ .mockResolvedValueOnce(new Response(null, { status: 204 }));
1263
+
1264
+ const { repository } = createApiV3CheerioRestIssueRepository();
1265
+ await repository.mergePullRequest(
1266
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1267
+ );
1268
+
1269
+ expect(fetchSpy).toHaveBeenCalledTimes(3);
1270
+ expect(fetchSpy).toHaveBeenNthCalledWith(
1271
+ 3,
1272
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/merge',
1273
+ expect.objectContaining({
1274
+ method: 'PUT',
1275
+ body: JSON.stringify({ merge_method: 'rebase' }),
1276
+ }),
1277
+ );
1278
+ });
1279
+
1280
+ it('should throw with the original error when no alternative merge method is available after HTTP 405', async () => {
1281
+ jest
1282
+ .spyOn(global, 'fetch')
1283
+ .mockResolvedValueOnce(
1284
+ new Response(
1285
+ JSON.stringify({
1286
+ message: 'Merge commits are not allowed on this repository.',
1287
+ }),
1288
+ { status: 405, headers: { 'Content-Type': 'application/json' } },
1289
+ ),
1290
+ )
1291
+ .mockResolvedValueOnce(
1292
+ new Response(
1293
+ JSON.stringify({
1294
+ allow_squash_merge: false,
1295
+ allow_rebase_merge: false,
1296
+ }),
1297
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1298
+ ),
1299
+ );
1300
+
1301
+ const { repository } = createApiV3CheerioRestIssueRepository();
1302
+ await expect(
1303
+ repository.mergePullRequest(
1304
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1305
+ ),
1306
+ ).rejects.toThrow(
1307
+ 'Failed to merge PR https://github.com/HiromiShikata/test-repository/pull/42: HTTP 405 Merge commits are not allowed on this repository.',
1308
+ );
1309
+ });
1310
+
1311
+ it('should throw when the merge API responds with a non-405 error', async () => {
1312
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1313
+ new Response(JSON.stringify({ message: 'Not Found' }), {
1314
+ status: 404,
1315
+ headers: { 'Content-Type': 'application/json' },
1316
+ }),
1317
+ );
1318
+
1319
+ const { repository } = createApiV3CheerioRestIssueRepository();
1320
+ await expect(
1321
+ repository.mergePullRequest(
1322
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1323
+ ),
1324
+ ).rejects.toThrow(
1325
+ 'Failed to merge PR https://github.com/HiromiShikata/test-repository/pull/42: HTTP 404 Not Found',
1326
+ );
1327
+ });
1328
+ });
1329
+
1174
1330
  describe('rate-limit-aware retry on console operations', () => {
1175
1331
  afterEach(() => {
1176
1332
  jest.restoreAllMocks();
@@ -362,6 +362,12 @@ function isRefContainer(value: unknown): value is { ref: string } {
362
362
  return isRecord(value) && typeof value.ref === 'string';
363
363
  }
364
364
 
365
+ function isRepoMergeSettings(
366
+ value: unknown,
367
+ ): value is { allow_squash_merge?: boolean; allow_rebase_merge?: boolean } {
368
+ return isRecord(value);
369
+ }
370
+
365
371
  type IssueOrPullRequestBodyResponse = {
366
372
  body: string | null;
367
373
  };
@@ -615,6 +621,10 @@ export class ApiV3CheerioRestIssueRepository
615
621
  const status = item.customFields.find(
616
622
  (field) => normalizeFieldName(field.name) === 'status',
617
623
  )?.value;
624
+ const agent =
625
+ item.customFields.find(
626
+ (field) => normalizeFieldName(field.name) === 'agent',
627
+ )?.value ?? null;
618
628
  const { owner, repo } = this.extractIssueFromUrl(item.url);
619
629
 
620
630
  return {
@@ -644,6 +654,7 @@ export class ApiV3CheerioRestIssueRepository
644
654
  createdAt: new Date(item.createdAt || '2000-01-01'),
645
655
  author: item.author,
646
656
  closingIssueReferenceUrls: item.closingIssueReferenceUrls,
657
+ agent,
647
658
  };
648
659
  };
649
660
  private restoreIssuesFromCache = (rawIssues: unknown): Issue[] | null => {
@@ -915,6 +926,33 @@ export class ApiV3CheerioRestIssueRepository
915
926
  );
916
927
  };
917
928
 
929
+ setIssueAgentField = async (
930
+ issueUrl: string,
931
+ project: Project,
932
+ agentOptionId: string,
933
+ ): Promise<void> => {
934
+ if (!project.agent) {
935
+ return;
936
+ }
937
+ const existingProjectItem =
938
+ await this.graphqlProjectItemRepository.fetchProjectItemByUrl(
939
+ issueUrl,
940
+ project.id,
941
+ );
942
+ const projectItemId =
943
+ existingProjectItem?.id ??
944
+ (await this.graphqlProjectItemRepository.addIssueToProject(
945
+ project.id,
946
+ issueUrl,
947
+ ));
948
+ await this.graphqlProjectItemRepository.updateProjectField(
949
+ project.id,
950
+ project.agent.fieldId,
951
+ projectItemId,
952
+ { singleSelectOptionId: agentOptionId },
953
+ );
954
+ };
955
+
918
956
  updateNextActionDate = async (
919
957
  issueUrl: string,
920
958
  project: Project,
@@ -2247,24 +2285,71 @@ export class ApiV3CheerioRestIssueRepository
2247
2285
 
2248
2286
  mergePullRequest = async (prUrl: string): Promise<void> => {
2249
2287
  const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
2288
+ const mergeUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/merge`;
2289
+ const headers = {
2290
+ Authorization: `Bearer ${this.ghToken}`,
2291
+ 'Content-Type': 'application/json',
2292
+ Accept: 'application/vnd.github+json',
2293
+ };
2294
+ const mergeWith = (mergeMethod?: 'squash' | 'rebase') =>
2295
+ this.fetchWithRateLimitRetry(() =>
2296
+ fetch(mergeUrl, {
2297
+ method: 'PUT',
2298
+ headers,
2299
+ body: JSON.stringify(
2300
+ mergeMethod ? { merge_method: mergeMethod } : {},
2301
+ ),
2302
+ }),
2303
+ );
2304
+ const response = await mergeWith();
2305
+ if (response.ok) {
2306
+ return;
2307
+ }
2308
+ if (response.status === 405) {
2309
+ const fallbackMethod = await this.resolveAllowedMergeMethod(owner, repo);
2310
+ if (fallbackMethod !== null) {
2311
+ const retryResponse = await mergeWith(fallbackMethod);
2312
+ if (retryResponse.ok) {
2313
+ return;
2314
+ }
2315
+ const retryReason =
2316
+ await this.formatGitHubErrorWithStatus(retryResponse);
2317
+ throw new Error(`Failed to merge PR ${prUrl}: ${retryReason}`);
2318
+ }
2319
+ }
2320
+ const reason = await this.formatGitHubErrorWithStatus(response);
2321
+ throw new Error(`Failed to merge PR ${prUrl}: ${reason}`);
2322
+ };
2323
+
2324
+ private resolveAllowedMergeMethod = async (
2325
+ owner: string,
2326
+ repo: string,
2327
+ ): Promise<'squash' | 'rebase' | null> => {
2250
2328
  const response = await this.fetchWithRateLimitRetry(() =>
2251
2329
  fetch(
2252
- `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/merge`,
2330
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
2253
2331
  {
2254
- method: 'PUT',
2255
2332
  headers: {
2256
2333
  Authorization: `Bearer ${this.ghToken}`,
2257
- 'Content-Type': 'application/json',
2258
2334
  Accept: 'application/vnd.github+json',
2259
2335
  },
2260
- body: JSON.stringify({}),
2261
2336
  },
2262
2337
  ),
2263
2338
  );
2264
2339
  if (!response.ok) {
2265
- const reason = await this.formatGitHubErrorWithStatus(response);
2266
- throw new Error(`Failed to merge PR ${prUrl}: ${reason}`);
2340
+ return null;
2267
2341
  }
2342
+ const data: unknown = await response.json();
2343
+ if (!isRepoMergeSettings(data)) {
2344
+ return null;
2345
+ }
2346
+ if (data.allow_squash_merge === true) {
2347
+ return 'squash';
2348
+ }
2349
+ if (data.allow_rebase_merge === true) {
2350
+ return 'rebase';
2351
+ }
2352
+ return null;
2268
2353
  };
2269
2354
 
2270
2355
  requestChangesWithInlineComment = async (
@@ -51,6 +51,7 @@ const buildIssue = (overrides: Partial<Issue> = {}): Issue => ({
51
51
  createdAt: new Date(),
52
52
  author: '',
53
53
  closingIssueReferenceUrls: [],
54
+ agent: null,
54
55
  ...overrides,
55
56
  });
56
57
 
@@ -140,6 +140,7 @@ describe('projectFromDefinition', () => {
140
140
  name: 'Completion Date (50% Confidence)',
141
141
  fieldId: 'PVTF_completion',
142
142
  },
143
+ agent: null,
143
144
  });
144
145
  });
145
146
 
@@ -1,5 +1,6 @@
1
1
  import { FieldOption, Project } from '../../domain/entities/Project';
2
2
  import {
3
+ AGENT_FIELD_NAME,
3
4
  DEPENDED_ISSUE_URL_FIELD_NAME,
4
5
  NEXT_ACTION_DATE_FIELD_NAME,
5
6
  NEXT_ACTION_HOUR_FIELD_NAME,
@@ -77,6 +78,10 @@ export const projectFromDefinition = (
77
78
  const completionDate50PercentConfidence = definition.fields.find((field) =>
78
79
  normalizeFieldName(field.name).startsWith('completiondate'),
79
80
  );
81
+ const agentField = definition.fields.find(
82
+ (field) =>
83
+ normalizeFieldName(field.name) === normalizeFieldName(AGENT_FIELD_NAME),
84
+ );
80
85
  return {
81
86
  id: definition.id,
82
87
  url: definition.url,
@@ -127,5 +132,12 @@ export const projectFromDefinition = (
127
132
  fieldId: completionDate50PercentConfidence.fieldId,
128
133
  }
129
134
  : null,
135
+ agent: agentField
136
+ ? {
137
+ name: agentField.name,
138
+ fieldId: agentField.fieldId,
139
+ options: agentField.options,
140
+ }
141
+ : null,
130
142
  };
131
143
  };
@@ -25,4 +25,5 @@ export type Issue = {
25
25
  createdAt: Date;
26
26
  author: string;
27
27
  closingIssueReferenceUrls: string[];
28
+ agent: string | null;
28
29
  };
@@ -47,4 +47,9 @@ export type Project = {
47
47
  name: string;
48
48
  fieldId: string;
49
49
  } | null;
50
+ agent: {
51
+ name: string;
52
+ fieldId: string;
53
+ options: FieldOption[];
54
+ } | null;
50
55
  };
@@ -40,6 +40,7 @@ export const NEXT_ACTION_DATE_FIELD_NAME = 'Next Action Date';
40
40
  export const NEXT_ACTION_HOUR_FIELD_NAME = 'Next Action Hour';
41
41
  export const DEPENDED_ISSUE_URL_FIELD_NAME =
42
42
  'Depended Issue URL separated by comma';
43
+ export const AGENT_FIELD_NAME = 'Agent';
43
44
 
44
45
  export const REQUIRED_PROJECT_FIELDS: RequiredProjectFieldDefinition[] = [
45
46
  {
@@ -62,4 +63,9 @@ export const REQUIRED_PROJECT_FIELDS: RequiredProjectFieldDefinition[] = [
62
63
  dataType: 'TEXT',
63
64
  options: [],
64
65
  },
66
+ {
67
+ name: AGENT_FIELD_NAME,
68
+ dataType: 'SINGLE_SELECT',
69
+ options: [],
70
+ },
65
71
  ];
@@ -28,6 +28,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
28
28
  createdAt: new Date('2000-01-01T00:00:00Z'),
29
29
  author: 'test-user',
30
30
  closingIssueReferenceUrls: [],
31
+ agent: null,
31
32
  ...overrides,
32
33
  });
33
34
 
@@ -41,6 +41,7 @@ describe('GetStoryObjectMapUseCase', () => {
41
41
  remainingEstimationMinutes: null,
42
42
  dependedIssueUrlSeparatedByComma: null,
43
43
  completionDate50PercentConfidence: null,
44
+ agent: null,
44
45
  };
45
46
 
46
47
  const createBasicIssue = (overrides: Partial<Issue> = {}): Issue => ({
@@ -68,6 +69,7 @@ describe('GetStoryObjectMapUseCase', () => {
68
69
  createdAt: new Date(),
69
70
  author: '',
70
71
  closingIssueReferenceUrls: [],
72
+ agent: null,
71
73
  ...overrides,
72
74
  });
73
75
 
@@ -683,6 +683,7 @@ describe('HandleScheduledEventUseCase', () => {
683
683
  remainingEstimationMinutes: null,
684
684
  dependedIssueUrlSeparatedByComma: null,
685
685
  completionDate50PercentConfidence: null,
686
+ agent: null,
686
687
  };
687
688
 
688
689
  const capturedLogs: string[] = [];
@@ -160,6 +160,7 @@ export class HandleScheduledEventUseCase {
160
160
  changeTargetPathAliases?: Record<string, string> | null;
161
161
  allowedIssueAuthors?: string[] | null;
162
162
  autoAssignManagerAuthors?: string[] | null;
163
+ agents?: string[] | null;
163
164
  startPreparation?: {
164
165
  defaultAgentName: string;
165
166
  defaultLlmModelName?: string | null;
@@ -195,6 +196,7 @@ export class HandleScheduledEventUseCase {
195
196
  }
196
197
  await this.projectRequiredFieldCreateUseCase.run({
197
198
  projectUrl: input.projectUrl,
199
+ agents: input.agents ?? null,
198
200
  });
199
201
  await this.setupTowerDefenceProjectUseCase.run({
200
202
  projectUrl: input.projectUrl,
@@ -51,6 +51,7 @@ const issue = (url: string, status: string): Issue => ({
51
51
  createdAt: new Date('2026-01-01T00:00:00Z'),
52
52
  author: 'hiromi',
53
53
  closingIssueReferenceUrls: [],
54
+ agent: null,
54
55
  });
55
56
 
56
57
  describe('InTmuxByHumanSessionTokenCountUseCase', () => {