github-issue-tower-defence-management 1.168.4 → 1.169.1

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 (36) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -0
  3. package/bin/adapter/entry-points/cli/index.js +4 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  7. package/bin/domain/usecases/AgentDesignationLabelAdoptUseCase.js +42 -0
  8. package/bin/domain/usecases/AgentDesignationLabelAdoptUseCase.js.map +1 -0
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -1
  10. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  11. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +41 -0
  12. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  13. package/bin/domain/usecases/StartPreparationUseCase.js +2 -34
  14. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/cli/index.test.ts +4 -0
  17. package/src/adapter/entry-points/cli/index.ts +12 -0
  18. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -0
  19. package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +221 -0
  20. package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.ts +76 -0
  21. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
  22. package/src/domain/usecases/HandleScheduledEventUseCase.ts +7 -0
  23. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +162 -0
  24. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +81 -0
  25. package/src/domain/usecases/StartPreparationUseCase.test.ts +43 -33
  26. package/src/domain/usecases/StartPreparationUseCase.ts +5 -58
  27. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  28. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  29. package/types/domain/usecases/AgentDesignationLabelAdoptUseCase.d.ts +16 -0
  30. package/types/domain/usecases/AgentDesignationLabelAdoptUseCase.d.ts.map +1 -0
  31. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
  32. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  33. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +4 -1
  34. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  35. package/types/domain/usecases/StartPreparationUseCase.d.ts +0 -1
  36. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
@@ -107,6 +107,8 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
107
107
  requestChangesWithInlineComment: jest.Mock;
108
108
  setDependedIssueUrl: jest.Mock;
109
109
  setIssueAgentField: jest.Mock;
110
+ searchIssue: jest.Mock;
111
+ createNewIssue: jest.Mock;
110
112
  };
111
113
  let mockIssueCommentRepository: {
112
114
  getCommentsFromIssue: jest.Mock;
@@ -147,6 +149,8 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
147
149
  requestChangesWithInlineComment: jest.fn().mockResolvedValue(undefined),
148
150
  setDependedIssueUrl: jest.fn(),
149
151
  setIssueAgentField: jest.fn().mockResolvedValue(undefined),
152
+ searchIssue: jest.fn().mockResolvedValue([]),
153
+ createNewIssue: jest.fn().mockResolvedValue(42),
150
154
  };
151
155
 
152
156
  mockIssueCommentRepository = {
@@ -3893,4 +3897,162 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
3893
3897
  ).not.toHaveBeenCalled();
3894
3898
  });
3895
3899
  });
3900
+
3901
+ describe('when missingAgentName is provided', () => {
3902
+ const issueUrl = 'https://github.com/user/repo/issues/1';
3903
+ const taskIssueTitle = 'Register missing agent definition: impl';
3904
+ const taskIssueUrl = 'https://github.com/user/repo/issues/42';
3905
+
3906
+ it('creates a task issue and sets depended issue URL when no existing open task issue exists', async () => {
3907
+ const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
3908
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
3909
+ mockIssueRepository.get.mockResolvedValue(issue);
3910
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
3911
+ mockIssueRepository.createNewIssue.mockResolvedValue(42);
3912
+
3913
+ await useCase.run({
3914
+ projectUrl: 'https://github.com/users/user/projects/1',
3915
+ issueUrl,
3916
+ thresholdForAutoReject: 3,
3917
+ workflowBlockerResolvedWebhookUrl: null,
3918
+ allowedIssueAuthors: null,
3919
+ missingAgentName: 'impl',
3920
+ sessionErrorLine:
3921
+ "Error: Agent 'impl' not found at /path/agents/impl.md",
3922
+ });
3923
+
3924
+ expect(mockIssueRepository.searchIssue).toHaveBeenCalledWith({
3925
+ owner: 'user',
3926
+ repositoryName: 'repo',
3927
+ type: 'issue',
3928
+ state: 'open',
3929
+ title: taskIssueTitle,
3930
+ });
3931
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledWith(
3932
+ 'user',
3933
+ 'repo',
3934
+ taskIssueTitle,
3935
+ expect.stringContaining(issueUrl),
3936
+ [],
3937
+ [],
3938
+ );
3939
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledWith(
3940
+ 'user',
3941
+ 'repo',
3942
+ taskIssueTitle,
3943
+ expect.stringContaining('impl'),
3944
+ [],
3945
+ [],
3946
+ );
3947
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledWith(
3948
+ 'user',
3949
+ 'repo',
3950
+ taskIssueTitle,
3951
+ expect.stringContaining(
3952
+ "Error: Agent 'impl' not found at /path/agents/impl.md",
3953
+ ),
3954
+ [],
3955
+ [],
3956
+ );
3957
+ expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
3958
+ issueUrl,
3959
+ mockProject,
3960
+ taskIssueUrl,
3961
+ );
3962
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
3963
+ mockProject,
3964
+ expect.objectContaining({ status: 'Awaiting Workspace' }),
3965
+ 'awaiting-workspace-id',
3966
+ );
3967
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
3968
+ expect.objectContaining({ url: issueUrl }),
3969
+ expect.stringContaining('impl'),
3970
+ );
3971
+ });
3972
+
3973
+ it('reuses an existing open task issue and does not create a new one', async () => {
3974
+ const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
3975
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
3976
+ mockIssueRepository.get.mockResolvedValue(issue);
3977
+ mockIssueRepository.searchIssue.mockResolvedValue([
3978
+ {
3979
+ url: taskIssueUrl,
3980
+ title: taskIssueTitle,
3981
+ number: '42',
3982
+ },
3983
+ ]);
3984
+
3985
+ await useCase.run({
3986
+ projectUrl: 'https://github.com/users/user/projects/1',
3987
+ issueUrl,
3988
+ thresholdForAutoReject: 3,
3989
+ workflowBlockerResolvedWebhookUrl: null,
3990
+ allowedIssueAuthors: null,
3991
+ missingAgentName: 'impl',
3992
+ });
3993
+
3994
+ expect(mockIssueRepository.createNewIssue).not.toHaveBeenCalled();
3995
+ expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
3996
+ issueUrl,
3997
+ mockProject,
3998
+ taskIssueUrl,
3999
+ );
4000
+ });
4001
+
4002
+ it('skips setDependedIssueUrl when project has no dependedIssueUrlSeparatedByComma field but still creates the task issue and returns to Awaiting Workspace', async () => {
4003
+ const projectWithoutDependedField = createMockProject({
4004
+ dependedIssueUrlSeparatedByComma: null,
4005
+ });
4006
+ const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
4007
+ mockProjectRepository.getByUrl.mockResolvedValue(
4008
+ projectWithoutDependedField,
4009
+ );
4010
+ mockIssueRepository.get.mockResolvedValue(issue);
4011
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
4012
+ mockIssueRepository.createNewIssue.mockResolvedValue(42);
4013
+
4014
+ await useCase.run({
4015
+ projectUrl: 'https://github.com/users/user/projects/1',
4016
+ issueUrl,
4017
+ thresholdForAutoReject: 3,
4018
+ workflowBlockerResolvedWebhookUrl: null,
4019
+ allowedIssueAuthors: null,
4020
+ missingAgentName: 'impl',
4021
+ });
4022
+
4023
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledTimes(1);
4024
+ expect(mockIssueRepository.setDependedIssueUrl).not.toHaveBeenCalled();
4025
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
4026
+ projectWithoutDependedField,
4027
+ expect.objectContaining({ status: 'Awaiting Workspace' }),
4028
+ 'awaiting-workspace-id',
4029
+ );
4030
+ });
4031
+
4032
+ it('uses (not captured) as error line when sessionErrorLine is not provided', async () => {
4033
+ const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
4034
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
4035
+ mockIssueRepository.get.mockResolvedValue(issue);
4036
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
4037
+ mockIssueRepository.createNewIssue.mockResolvedValue(42);
4038
+
4039
+ await useCase.run({
4040
+ projectUrl: 'https://github.com/users/user/projects/1',
4041
+ issueUrl,
4042
+ thresholdForAutoReject: 3,
4043
+ workflowBlockerResolvedWebhookUrl: null,
4044
+ allowedIssueAuthors: null,
4045
+ missingAgentName: 'impl',
4046
+ });
4047
+
4048
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledWith(
4049
+ 'user',
4050
+ 'repo',
4051
+ taskIssueTitle,
4052
+ expect.stringContaining('(not captured)'),
4053
+ [],
4054
+ [],
4055
+ );
4056
+ });
4057
+ });
3896
4058
  });
@@ -73,6 +73,8 @@ export class NotifyFinishedIssuePreparationUseCase {
73
73
  | 'requestChangesWithInlineComment'
74
74
  | 'setDependedIssueUrl'
75
75
  | 'setIssueAgentField'
76
+ | 'searchIssue'
77
+ | 'createNewIssue'
76
78
  >,
77
79
  private readonly issueCommentRepository: Pick<
78
80
  IssueCommentRepository,
@@ -100,6 +102,8 @@ export class NotifyFinishedIssuePreparationUseCase {
100
102
  labelsNotRequiringPullRequest?: string[] | null;
101
103
  changeTargetPathAliases?: Record<string, string> | null;
102
104
  agents?: string[] | null;
105
+ missingAgentName?: string | null;
106
+ sessionErrorLine?: string | null;
103
107
  }): Promise<void> => {
104
108
  const project = await this.projectRepository.getByUrl(params.projectUrl);
105
109
 
@@ -143,6 +147,17 @@ export class NotifyFinishedIssuePreparationUseCase {
143
147
  );
144
148
  }
145
149
 
150
+ if (params.missingAgentName) {
151
+ await this.handleMissingAgentDefinition(
152
+ issue,
153
+ project,
154
+ awaitingWorkspaceStatusOption,
155
+ params.missingAgentName,
156
+ params.sessionErrorLine ?? null,
157
+ );
158
+ return;
159
+ }
160
+
146
161
  if (issue.dependedIssueUrls.length === 0) {
147
162
  try {
148
163
  const storyObjectMap =
@@ -363,6 +378,72 @@ export class NotifyFinishedIssuePreparationUseCase {
363
378
  );
364
379
  };
365
380
 
381
+ private handleMissingAgentDefinition = async (
382
+ issue: Issue,
383
+ project: Project,
384
+ awaitingWorkspaceStatusOption: { id: string },
385
+ missingAgentName: string,
386
+ sessionErrorLine: string | null,
387
+ ): Promise<void> => {
388
+ const taskIssueTitle = `Register missing agent definition: ${missingAgentName}`;
389
+
390
+ const searchResults = await this.issueRepository.searchIssue({
391
+ owner: issue.org,
392
+ repositoryName: issue.repo,
393
+ type: 'issue',
394
+ state: 'open',
395
+ title: taskIssueTitle,
396
+ });
397
+ const exactMatch = searchResults.find((i) => i.title === taskIssueTitle);
398
+
399
+ let taskIssueUrl: string;
400
+ if (exactMatch) {
401
+ taskIssueUrl = exactMatch.url;
402
+ } else {
403
+ const body = [
404
+ `The preparation worker for ${issue.url} failed because the agent definition \`${missingAgentName}\` was not found.`,
405
+ '',
406
+ `- Missing agent name: \`${missingAgentName}\``,
407
+ `- Failing item: ${issue.url}`,
408
+ `- Error: ${sessionErrorLine ?? '(not captured)'}`,
409
+ ].join('\n');
410
+ const issueNumber = await this.issueRepository.createNewIssue(
411
+ issue.org,
412
+ issue.repo,
413
+ taskIssueTitle,
414
+ body,
415
+ [],
416
+ [],
417
+ );
418
+ taskIssueUrl = `https://github.com/${issue.org}/${issue.repo}/issues/${issueNumber}`;
419
+ }
420
+
421
+ if (project.dependedIssueUrlSeparatedByComma) {
422
+ await this.issueRepository.setDependedIssueUrl(
423
+ issue.url,
424
+ project,
425
+ taskIssueUrl,
426
+ );
427
+ } else {
428
+ console.warn(
429
+ `dependedIssueUrlSeparatedByComma not configured; cannot block ${issue.url} via ${taskIssueUrl}`,
430
+ );
431
+ }
432
+
433
+ issue.status = AWAITING_WORKSPACE_STATUS_NAME;
434
+ await this.issueRepository.update(issue, project);
435
+ await this.issueRepository.updateStatus(
436
+ project,
437
+ issue,
438
+ awaitingWorkspaceStatusOption.id,
439
+ );
440
+ await this.patchConsoleTab(issue);
441
+ await this.issueCommentRepository.createComment(
442
+ issue,
443
+ `Session ended: agent definition \`${missingAgentName}\` was not found.\nItem blocked until the following task issue is resolved:\n${taskIssueUrl}`,
444
+ );
445
+ };
446
+
366
447
  private isAuthorTrusted = (
367
448
  author: string,
368
449
  allowedIssueAuthors: string[] | null,
@@ -189,7 +189,7 @@ describe('StartPreparationUseCase', () => {
189
189
  'aw',
190
190
  [
191
191
  'url1',
192
- 'impl',
192
+ 'agent1',
193
193
  'claude-opus',
194
194
  '--configFilePath',
195
195
  '/path/to/config.yml',
@@ -359,7 +359,7 @@ describe('StartPreparationUseCase', () => {
359
359
  expect(mockIssueRepository.setIssueAgentField.mock.calls).toHaveLength(0);
360
360
  expect(mockIssueRepository.removeLabel.mock.calls).toHaveLength(0);
361
361
  expect(mockLocalCommandRunner.runCommand.mock.calls[0][1][1]).toBe(
362
- 'triager',
362
+ 'agent1',
363
363
  );
364
364
  });
365
365
 
@@ -507,7 +507,7 @@ describe('StartPreparationUseCase', () => {
507
507
  'aw',
508
508
  [
509
509
  'url1',
510
- 'impl',
510
+ 'agent1',
511
511
  'claude-opus',
512
512
  '--configFilePath',
513
513
  '/path/to/config.yml',
@@ -566,7 +566,7 @@ describe('StartPreparationUseCase', () => {
566
566
  'aw',
567
567
  [
568
568
  'https://github.com/user/repo/pull/354',
569
- 'impl',
569
+ 'agent1',
570
570
  'claude-opus',
571
571
  '--configFilePath',
572
572
  '/path/to/config.yml',
@@ -803,7 +803,7 @@ describe('StartPreparationUseCase', () => {
803
803
  'aw',
804
804
  [
805
805
  'https://github.com/user/repo/issues/1',
806
- 'impl',
806
+ 'agent1',
807
807
  'claude-opus',
808
808
  '--configFilePath',
809
809
  '/path/to/config.yml',
@@ -1081,7 +1081,7 @@ describe('StartPreparationUseCase', () => {
1081
1081
  'aw',
1082
1082
  [
1083
1083
  'url1',
1084
- 'impl',
1084
+ 'agent1',
1085
1085
  'claude-opus',
1086
1086
  '--configFilePath',
1087
1087
  '/path/to/config.yml',
@@ -1127,7 +1127,7 @@ describe('StartPreparationUseCase', () => {
1127
1127
  'aw',
1128
1128
  [
1129
1129
  'url1',
1130
- 'impl',
1130
+ 'agent1',
1131
1131
  'claude-opus',
1132
1132
  '--configFilePath',
1133
1133
  '/path/to/config.yml',
@@ -1136,7 +1136,7 @@ describe('StartPreparationUseCase', () => {
1136
1136
  ],
1137
1137
  ]);
1138
1138
  });
1139
- it('should use llm-agent label over category label and defaultLlmAgentName', async () => {
1139
+ it('falls back to defaultAgentName when Agent field is empty, ignoring llm-agent: labels', async () => {
1140
1140
  const awaitingIssues: Issue[] = [
1141
1141
  createMockIssue({
1142
1142
  url: 'url1',
@@ -1173,7 +1173,7 @@ describe('StartPreparationUseCase', () => {
1173
1173
  'aw',
1174
1174
  [
1175
1175
  'url1',
1176
- 'research',
1176
+ 'agent1',
1177
1177
  'claude-sonnet-4-6',
1178
1178
  '--configFilePath',
1179
1179
  '/path/to/config.yml',
@@ -1182,7 +1182,7 @@ describe('StartPreparationUseCase', () => {
1182
1182
  ],
1183
1183
  ]);
1184
1184
  });
1185
- it('should use category label over defaultLlmAgentName when no llm-agent label', async () => {
1185
+ it('falls back to defaultAgentName when Agent field is empty, ignoring category: labels', async () => {
1186
1186
  const awaitingIssues: Issue[] = [
1187
1187
  createMockIssue({
1188
1188
  url: 'url1',
@@ -1219,7 +1219,7 @@ describe('StartPreparationUseCase', () => {
1219
1219
  'aw',
1220
1220
  [
1221
1221
  'url1',
1222
- 'impl',
1222
+ 'agent1',
1223
1223
  'claude-sonnet-4-6',
1224
1224
  '--configFilePath',
1225
1225
  '/path/to/config.yml',
@@ -1228,7 +1228,7 @@ describe('StartPreparationUseCase', () => {
1228
1228
  ],
1229
1229
  ]);
1230
1230
  });
1231
- it('should use defaultLlmAgentName over defaultAgentName when no label', async () => {
1231
+ it('falls back to defaultAgentName when Agent field is empty, ignoring defaultLlmAgentName', async () => {
1232
1232
  const awaitingIssues: Issue[] = [
1233
1233
  createMockIssue({
1234
1234
  url: 'url1',
@@ -1265,7 +1265,7 @@ describe('StartPreparationUseCase', () => {
1265
1265
  'aw',
1266
1266
  [
1267
1267
  'url1',
1268
- 'default-llm-agent',
1268
+ 'agent1',
1269
1269
  'claude-sonnet-4-6',
1270
1270
  '--configFilePath',
1271
1271
  '/path/to/config.yml',
@@ -1311,7 +1311,7 @@ describe('StartPreparationUseCase', () => {
1311
1311
  'aw',
1312
1312
  [
1313
1313
  'url1',
1314
- 'impl',
1314
+ 'agent1',
1315
1315
  'claude-sonnet',
1316
1316
  '--configFilePath',
1317
1317
  '/path/to/config.yml',
@@ -1412,7 +1412,7 @@ describe('StartPreparationUseCase', () => {
1412
1412
  'aw',
1413
1413
  [
1414
1414
  'url2',
1415
- 'impl',
1415
+ 'agent1',
1416
1416
  'claude-sonnet-4-6',
1417
1417
  '--configFilePath',
1418
1418
  '/path/to/config.yml',
@@ -2594,7 +2594,7 @@ describe('StartPreparationUseCase', () => {
2594
2594
  'aw',
2595
2595
  [
2596
2596
  'url1',
2597
- 'impl',
2597
+ 'agent1',
2598
2598
  'claude-opus',
2599
2599
  '--configFilePath',
2600
2600
  '/path/to/config.yml',
@@ -2643,7 +2643,7 @@ describe('StartPreparationUseCase', () => {
2643
2643
  'aw',
2644
2644
  [
2645
2645
  'url1',
2646
- 'impl',
2646
+ 'agent1',
2647
2647
  'claude-opus',
2648
2648
  '--configFilePath',
2649
2649
  '/path/to/config.yml',
@@ -2692,7 +2692,7 @@ describe('StartPreparationUseCase', () => {
2692
2692
  'aw',
2693
2693
  [
2694
2694
  'url1',
2695
- 'impl',
2695
+ 'agent1',
2696
2696
  'claude-opus',
2697
2697
  '--configFilePath',
2698
2698
  '/path/to/config.yml',
@@ -5201,54 +5201,54 @@ describe('StartPreparationUseCase', () => {
5201
5201
  expect(selectedAgent).toBe('developer');
5202
5202
  });
5203
5203
 
5204
- it('selects explicit llm-agent: label over labelsAsLlmAgentName mapping', async () => {
5204
+ it('uses defaultAgentName when Agent field is empty, ignoring llm-agent: label and labelsAsLlmAgentName', async () => {
5205
5205
  const selectedAgent = await runWithIssueLabels({
5206
5206
  labels: ['llm-agent:explicit-agent', 'story', 'category:impl'],
5207
5207
  defaultAgentName: 'default-agent',
5208
5208
  defaultLlmAgentName: 'default-llm-agent',
5209
5209
  labelsAsLlmAgentName: ['story'],
5210
5210
  });
5211
- expect(selectedAgent).toBe('explicit-agent');
5211
+ expect(selectedAgent).toBe('default-agent');
5212
5212
  });
5213
5213
 
5214
- it('uses the label name as the agent name when an issue label is listed in labelsAsLlmAgentName, over category: label', async () => {
5214
+ it('uses defaultAgentName when Agent field is empty, ignoring labelsAsLlmAgentName and category: label', async () => {
5215
5215
  const selectedAgent = await runWithIssueLabels({
5216
5216
  labels: ['story', 'category:impl'],
5217
5217
  defaultAgentName: 'default-agent',
5218
5218
  defaultLlmAgentName: 'default-llm-agent',
5219
5219
  labelsAsLlmAgentName: ['story'],
5220
5220
  });
5221
- expect(selectedAgent).toBe('story');
5221
+ expect(selectedAgent).toBe('default-agent');
5222
5222
  });
5223
5223
 
5224
- it('matches labelsAsLlmAgentName entries exactly including colons in the label name', async () => {
5224
+ it('uses defaultAgentName when Agent field is empty, ignoring colon-containing label in labelsAsLlmAgentName', async () => {
5225
5225
  const selectedAgent = await runWithIssueLabels({
5226
5226
  labels: ['story:body-condition', 'category:impl'],
5227
5227
  defaultAgentName: 'default-agent',
5228
5228
  defaultLlmAgentName: 'default-llm-agent',
5229
5229
  labelsAsLlmAgentName: ['story', 'story:body-condition'],
5230
5230
  });
5231
- expect(selectedAgent).toBe('story:body-condition');
5231
+ expect(selectedAgent).toBe('default-agent');
5232
5232
  });
5233
5233
 
5234
- it('falls through to category: label when no llm-agent: label and no issue label is in labelsAsLlmAgentName', async () => {
5234
+ it('uses defaultAgentName when Agent field is empty, ignoring category: label and unmatched labelsAsLlmAgentName', async () => {
5235
5235
  const selectedAgent = await runWithIssueLabels({
5236
5236
  labels: ['unrelated-label', 'category:impl'],
5237
5237
  defaultAgentName: 'default-agent',
5238
5238
  defaultLlmAgentName: 'default-llm-agent',
5239
5239
  labelsAsLlmAgentName: ['story'],
5240
5240
  });
5241
- expect(selectedAgent).toBe('impl');
5241
+ expect(selectedAgent).toBe('default-agent');
5242
5242
  });
5243
5243
 
5244
- it('falls through to defaultLlmAgentName when no llm-agent:, no labelsAsLlmAgentName match, and no category: label', async () => {
5244
+ it('uses defaultAgentName when Agent field is empty, ignoring defaultLlmAgentName', async () => {
5245
5245
  const selectedAgent = await runWithIssueLabels({
5246
5246
  labels: ['unrelated-label'],
5247
5247
  defaultAgentName: 'default-agent',
5248
5248
  defaultLlmAgentName: 'default-llm-agent',
5249
5249
  labelsAsLlmAgentName: ['story'],
5250
5250
  });
5251
- expect(selectedAgent).toBe('default-llm-agent');
5251
+ expect(selectedAgent).toBe('default-agent');
5252
5252
  });
5253
5253
 
5254
5254
  it('falls through to defaultAgentName when no llm-agent:, no labelsAsLlmAgentName match, no category: label, and no defaultLlmAgentName', async () => {
@@ -5268,7 +5268,7 @@ describe('StartPreparationUseCase', () => {
5268
5268
  defaultLlmAgentName: 'default-llm-agent',
5269
5269
  labelsAsLlmAgentName: ['story'],
5270
5270
  });
5271
- expect(selectedAgent).toBe('default-llm-agent');
5271
+ expect(selectedAgent).toBe('default-agent');
5272
5272
  });
5273
5273
 
5274
5274
  it('ignores labels that are not listed in labelsAsLlmAgentName when other entries are present', async () => {
@@ -5278,17 +5278,27 @@ describe('StartPreparationUseCase', () => {
5278
5278
  defaultLlmAgentName: 'default-llm-agent',
5279
5279
  labelsAsLlmAgentName: ['story', 'story:body-condition'],
5280
5280
  });
5281
- expect(selectedAgent).toBe('default-llm-agent');
5281
+ expect(selectedAgent).toBe('default-agent');
5282
5282
  });
5283
5283
 
5284
- it('does not affect selection when labelsAsLlmAgentName is null and only category: label is present', async () => {
5284
+ it('uses defaultAgentName when labelsAsLlmAgentName is null and Agent field is empty', async () => {
5285
5285
  const selectedAgent = await runWithIssueLabels({
5286
5286
  labels: ['category:impl'],
5287
5287
  defaultAgentName: 'default-agent',
5288
5288
  defaultLlmAgentName: 'default-llm-agent',
5289
5289
  labelsAsLlmAgentName: null,
5290
5290
  });
5291
- expect(selectedAgent).toBe('impl');
5291
+ expect(selectedAgent).toBe('default-agent');
5292
+ });
5293
+
5294
+ it('uses defaultAgentName when Agent field is empty and a bare label matches labelsAsLlmAgentName', async () => {
5295
+ const selectedAgent = await runWithIssueLabels({
5296
+ labels: ['chore'],
5297
+ defaultAgentName: 'triage-agent',
5298
+ defaultLlmAgentName: 'default-llm-agent',
5299
+ labelsAsLlmAgentName: ['chore'],
5300
+ });
5301
+ expect(selectedAgent).toBe('triage-agent');
5292
5302
  });
5293
5303
  });
5294
5304
 
@@ -5347,7 +5357,7 @@ describe('StartPreparationUseCase', () => {
5347
5357
  expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
5348
5358
  expect(mockLocalCommandRunner.runCommand.mock.calls[0][1]).toEqual([
5349
5359
  'url1',
5350
- 'impl',
5360
+ 'agent1',
5351
5361
  'claude-opus-4-8',
5352
5362
  '--configFilePath',
5353
5363
  '/path/to/config.yml',
@@ -9,9 +9,7 @@ import {
9
9
  AWAITING_WORKSPACE_STATUS_NAME,
10
10
  PREPARATION_STATUS_NAME,
11
11
  } from '../entities/WorkflowStatus';
12
- import { ensureAgentOptionAndGetId } from './ensureAgentOptionAndGetId';
13
- import { Issue } from '../entities/Issue';
14
- import { Project } from '../entities/Project';
12
+ import { adoptIssueAgentDesignationLabel } from './AgentDesignationLabelAdoptUseCase';
15
13
 
16
14
  const NORMAL_CONCURRENT_LIMIT = 6;
17
15
  const SEVEN_DAY_THROTTLE_START_THRESHOLD = 0.8;
@@ -310,41 +308,6 @@ export class StartPreparationUseCase {
310
308
  return [...selectedEntries, ...excluded];
311
309
  };
312
310
 
313
- private migrateAgentDesignationLabelToProjectField = async (
314
- issue: Issue,
315
- project: Project,
316
- configuredAgentNames: string[] | null,
317
- ): Promise<void> => {
318
- const agentLabel =
319
- configuredAgentNames === null
320
- ? undefined
321
- : issue.labels.find((label: string) =>
322
- configuredAgentNames.includes(label),
323
- );
324
- if (agentLabel === undefined) {
325
- return;
326
- }
327
- issue.agent = agentLabel;
328
- const agentOptionId = await ensureAgentOptionAndGetId(
329
- this.projectRepository,
330
- project,
331
- agentLabel,
332
- );
333
- if (agentOptionId === null) {
334
- console.warn(
335
- `Agent field option '${agentLabel}' could not be resolved for ${issue.url}. Keeping the label as the agent designation.`,
336
- );
337
- return;
338
- }
339
- await this.issueRepository.setIssueAgentField(
340
- issue.url,
341
- project,
342
- agentOptionId,
343
- );
344
- await this.issueRepository.removeLabel(issue, agentLabel);
345
- issue.labels = issue.labels.filter((label) => label !== agentLabel);
346
- };
347
-
348
311
  run = async (params: {
349
312
  projectUrl: string;
350
313
  defaultAgentName: string;
@@ -520,31 +483,15 @@ export class StartPreparationUseCase {
520
483
  exclusionCounts.notAssignedToManager++;
521
484
  continue;
522
485
  }
523
- await this.migrateAgentDesignationLabelToProjectField(
486
+ await adoptIssueAgentDesignationLabel(
524
487
  issue,
525
488
  project,
526
- params.agents ?? null,
489
+ params.agents ?? [],
490
+ this.projectRepository,
491
+ this.issueRepository,
527
492
  );
528
- const mappedAgentFromLabel =
529
- params.labelsAsLlmAgentName !== null
530
- ? issue.labels.find((label: string) =>
531
- params.labelsAsLlmAgentName !== null
532
- ? params.labelsAsLlmAgentName.includes(label)
533
- : false,
534
- )
535
- : undefined;
536
493
  const agent =
537
494
  (issue.agent === null ? null : agentNameFromDesignation(issue.agent)) ||
538
- issue.labels
539
- .find((label: string) => label.startsWith(LLM_AGENT_LABEL_PREFIX))
540
- ?.replace(LLM_AGENT_LABEL_PREFIX, '')
541
- .trim() ||
542
- mappedAgentFromLabel ||
543
- issue.labels
544
- .find((label: string) => label.startsWith('category:'))
545
- ?.replace('category:', '')
546
- .trim() ||
547
- params.defaultLlmAgentName ||
548
495
  params.defaultAgentName;
549
496
  const labelModelName = issue.labels
550
497
  .find((label: string) => label.startsWith('llm-model:'))
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AA8LzB,eAAO,MAAM,OAAO,SAAgB,CAAC;AA8gCrC,eAAO,MAAM,uBAAuB,GAAI,OAAO,OAAO,KAAG,IAGxD,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EAAE,EACd,kBAAkB,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KACzC,OAAO,CAAC,IAAI,CAMd,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAgMzB,eAAO,MAAM,OAAO,SAAgB,CAAC;AAwhCrC,eAAO,MAAM,uBAAuB,GAAI,OAAO,OAAO,KAAG,IAGxD,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EAAE,EACd,kBAAkB,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KACzC,OAAO,CAAC,IAAI,CAMd,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAiDA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAqD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,EACjB,6BAA4B,MAAM,EAAE,GAAG,IAAW,KACjD,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CA2xBP;CACH"}
1
+ {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAiDA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAsD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,EACjB,6BAA4B,MAAM,EAAE,GAAG,IAAW,KACjD,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CA8xBP;CACH"}
@@ -0,0 +1,16 @@
1
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
2
+ import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
3
+ import { Issue } from '../entities/Issue';
4
+ import { Project } from '../entities/Project';
5
+ export declare const adoptIssueAgentDesignationLabel: (issue: Issue, project: Project, configuredAgentNames: string[], projectRepository: Pick<ProjectRepository, "getByUrl" | "createField" | "updateAgentList">, issueRepository: Pick<IssueRepository, "setIssueAgentField" | "removeLabel">) => Promise<void>;
6
+ export declare class AgentDesignationLabelAdoptUseCase {
7
+ private readonly projectRepository;
8
+ private readonly issueRepository;
9
+ constructor(projectRepository: Pick<ProjectRepository, 'getByUrl' | 'createField' | 'updateAgentList'>, issueRepository: Pick<IssueRepository, 'setIssueAgentField' | 'removeLabel'>);
10
+ run: (params: {
11
+ project: Project;
12
+ issues: Issue[];
13
+ agents: string[] | null;
14
+ }) => Promise<void>;
15
+ }
16
+ //# sourceMappingURL=AgentDesignationLabelAdoptUseCase.d.ts.map