github-issue-tower-defence-management 1.163.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 (116) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/bin/adapter/entry-points/cli/index.js +1 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleOperationApi.js +1 -0
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
  10. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  11. package/bin/adapter/repositories/GraphqlProjectRepository.js +37 -0
  12. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  13. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +59 -5
  14. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  15. package/bin/adapter/repositories/issue/RestIssueRepository.js +24 -0
  16. package/bin/adapter/repositories/issue/RestIssueRepository.js.map +1 -1
  17. package/bin/adapter/repositories/projectFieldDefinition.js +8 -0
  18. package/bin/adapter/repositories/projectFieldDefinition.js.map +1 -1
  19. package/bin/domain/entities/RequiredProjectField.js +7 -1
  20. package/bin/domain/entities/RequiredProjectField.js.map +1 -1
  21. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  22. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  23. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +67 -0
  24. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  25. package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +28 -2
  26. package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -1
  27. package/bin/domain/usecases/StartPreparationUseCase.js +5 -4
  28. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  29. package/lib/typescript-compat/bin/tsc +2 -0
  30. package/lib/typescript-compat/bin/tsserver +2 -0
  31. package/lib/typescript-compat/lib/typescript.js +1 -0
  32. package/lib/typescript-compat/package.json +13 -0
  33. package/package.json +4 -3
  34. package/src/adapter/entry-points/cli/index.test.ts +2 -0
  35. package/src/adapter/entry-points/cli/index.ts +1 -0
  36. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  37. package/src/adapter/entry-points/console/consoleOperationApi.ts +1 -0
  38. package/src/adapter/entry-points/console/consoleTabNames.test.ts +1 -0
  39. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
  40. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +4 -0
  41. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
  42. package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
  43. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +2 -0
  44. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  45. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
  46. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
  47. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
  48. package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +2 -0
  49. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
  50. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
  51. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
  52. package/src/adapter/repositories/GraphqlProjectRepository.diskCache.test.ts +1 -0
  53. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +2 -0
  54. package/src/adapter/repositories/GraphqlProjectRepository.ts +50 -0
  55. package/src/adapter/repositories/GraphqlProjectRepositoryProjectLocation.test.ts +1 -0
  56. package/src/adapter/repositories/ProjectIssuesCacheRepository.test.ts +1 -0
  57. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +156 -0
  58. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +99 -6
  59. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
  60. package/src/adapter/repositories/issue/RestIssueRepository.ts +30 -0
  61. package/src/adapter/repositories/projectFieldDefinition.test.ts +1 -0
  62. package/src/adapter/repositories/projectFieldDefinition.ts +12 -0
  63. package/src/domain/entities/Issue.ts +1 -0
  64. package/src/domain/entities/Project.ts +5 -0
  65. package/src/domain/entities/RequiredProjectField.ts +6 -0
  66. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
  67. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +2 -0
  68. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +1 -0
  69. package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
  70. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
  71. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +381 -0
  72. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +117 -1
  73. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
  74. package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +75 -1
  75. package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +50 -7
  76. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +2 -0
  77. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +2 -0
  78. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +2 -0
  79. package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +2 -0
  80. package/src/domain/usecases/StartPreparationUseCase.test.ts +2 -0
  81. package/src/domain/usecases/StartPreparationUseCase.ts +1 -0
  82. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +10 -0
  83. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
  84. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +2 -0
  85. package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
  86. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +2 -0
  87. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  88. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  89. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  90. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  91. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  92. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  93. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +3 -0
  94. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  95. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +5 -2
  96. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  97. package/types/adapter/repositories/issue/RestIssueRepository.d.ts +1 -0
  98. package/types/adapter/repositories/issue/RestIssueRepository.d.ts.map +1 -1
  99. package/types/adapter/repositories/projectFieldDefinition.d.ts.map +1 -1
  100. package/types/domain/entities/Issue.d.ts +1 -0
  101. package/types/domain/entities/Issue.d.ts.map +1 -1
  102. package/types/domain/entities/Project.d.ts +5 -0
  103. package/types/domain/entities/Project.d.ts.map +1 -1
  104. package/types/domain/entities/RequiredProjectField.d.ts +1 -0
  105. package/types/domain/entities/RequiredProjectField.d.ts.map +1 -1
  106. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  107. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  108. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +4 -1
  109. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  110. package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts +5 -1
  111. package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -1
  112. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  113. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -0
  114. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  115. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +3 -0
  116. 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
  };
@@ -527,6 +533,7 @@ export class ApiV3CheerioRestIssueRepository
527
533
  | 'removeLabel'
528
534
  | 'updateAssigneeList'
529
535
  | 'searchIssues'
536
+ | 'getOrCreateLabel'
530
537
  >,
531
538
  readonly graphqlProjectItemRepository: Pick<
532
539
  GraphqlProjectItemRepository,
@@ -614,6 +621,10 @@ export class ApiV3CheerioRestIssueRepository
614
621
  const status = item.customFields.find(
615
622
  (field) => normalizeFieldName(field.name) === 'status',
616
623
  )?.value;
624
+ const agent =
625
+ item.customFields.find(
626
+ (field) => normalizeFieldName(field.name) === 'agent',
627
+ )?.value ?? null;
617
628
  const { owner, repo } = this.extractIssueFromUrl(item.url);
618
629
 
619
630
  return {
@@ -643,6 +654,7 @@ export class ApiV3CheerioRestIssueRepository
643
654
  createdAt: new Date(item.createdAt || '2000-01-01'),
644
655
  author: item.author,
645
656
  closingIssueReferenceUrls: item.closingIssueReferenceUrls,
657
+ agent,
646
658
  };
647
659
  };
648
660
  private restoreIssuesFromCache = (rawIssues: unknown): Issue[] | null => {
@@ -914,6 +926,33 @@ export class ApiV3CheerioRestIssueRepository
914
926
  );
915
927
  };
916
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
+
917
956
  updateNextActionDate = async (
918
957
  issueUrl: string,
919
958
  project: Project,
@@ -1006,6 +1045,13 @@ export class ApiV3CheerioRestIssueRepository
1006
1045
  removeLabel = (issue: Issue, label: string): Promise<void> => {
1007
1046
  return this.restIssueRepository.removeLabel(issue, label);
1008
1047
  };
1048
+ getOrCreateLabel = (
1049
+ org: string,
1050
+ repo: string,
1051
+ labelName: string,
1052
+ ): Promise<void> => {
1053
+ return this.restIssueRepository.getOrCreateLabel(org, repo, labelName);
1054
+ };
1009
1055
  updateAssigneeList = (
1010
1056
  issue: Pick<Issue, 'org' | 'repo' | 'number'>,
1011
1057
  assigneeList: Member['name'][],
@@ -2239,24 +2285,71 @@ export class ApiV3CheerioRestIssueRepository
2239
2285
 
2240
2286
  mergePullRequest = async (prUrl: string): Promise<void> => {
2241
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> => {
2242
2328
  const response = await this.fetchWithRateLimitRetry(() =>
2243
2329
  fetch(
2244
- `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/merge`,
2330
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
2245
2331
  {
2246
- method: 'PUT',
2247
2332
  headers: {
2248
2333
  Authorization: `Bearer ${this.ghToken}`,
2249
- 'Content-Type': 'application/json',
2250
2334
  Accept: 'application/vnd.github+json',
2251
2335
  },
2252
- body: JSON.stringify({}),
2253
2336
  },
2254
2337
  ),
2255
2338
  );
2256
2339
  if (!response.ok) {
2257
- const reason = await this.formatGitHubErrorWithStatus(response);
2258
- throw new Error(`Failed to merge PR ${prUrl}: ${reason}`);
2340
+ return null;
2341
+ }
2342
+ const data: unknown = await response.json();
2343
+ if (!isRepoMergeSettings(data)) {
2344
+ return null;
2259
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;
2260
2353
  };
2261
2354
 
2262
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
 
@@ -154,6 +154,36 @@ export class RestIssueRepository
154
154
  }
155
155
  };
156
156
 
157
+ getOrCreateLabel = async (
158
+ org: string,
159
+ repo: string,
160
+ labelName: string,
161
+ ): Promise<void> => {
162
+ try {
163
+ await ky.get(
164
+ `https://api.github.com/repos/${org}/${repo}/labels/${encodeURIComponent(labelName)}`,
165
+ {
166
+ headers: {
167
+ Authorization: `token ${this.ghToken}`,
168
+ Accept: 'application/vnd.github.v3+json',
169
+ },
170
+ },
171
+ );
172
+ } catch (e) {
173
+ if (e instanceof HTTPError && e.response.status === 404) {
174
+ await ky.post(`https://api.github.com/repos/${org}/${repo}/labels`, {
175
+ json: { name: labelName, color: 'ededed' },
176
+ headers: {
177
+ Authorization: `token ${this.ghToken}`,
178
+ Accept: 'application/vnd.github.v3+json',
179
+ },
180
+ });
181
+ } else {
182
+ throw e;
183
+ }
184
+ }
185
+ };
186
+
157
187
  updateAssigneeList = async (
158
188
  issue: Pick<Issue, 'org' | 'repo' | 'number'>,
159
189
  assigneeList: Member['name'][],
@@ -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', () => {