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.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/cli/index.js +4 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/AgentDesignationLabelAdoptUseCase.js +42 -0
- package/bin/domain/usecases/AgentDesignationLabelAdoptUseCase.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +41 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +2 -34
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +4 -0
- package/src/adapter/entry-points/cli/index.ts +12 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -0
- package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +221 -0
- package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.ts +76 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +7 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +162 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +81 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +43 -33
- package/src/domain/usecases/StartPreparationUseCase.ts +5 -58
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/domain/usecases/AgentDesignationLabelAdoptUseCase.d.ts +16 -0
- package/types/domain/usecases/AgentDesignationLabelAdoptUseCase.d.ts.map +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +4 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +0 -1
- 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
|
-
'
|
|
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
|
-
'
|
|
362
|
+
'agent1',
|
|
363
363
|
);
|
|
364
364
|
});
|
|
365
365
|
|
|
@@ -507,7 +507,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
507
507
|
'aw',
|
|
508
508
|
[
|
|
509
509
|
'url1',
|
|
510
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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('
|
|
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
|
-
'
|
|
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('
|
|
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
|
-
'
|
|
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('
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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('
|
|
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('
|
|
5211
|
+
expect(selectedAgent).toBe('default-agent');
|
|
5212
5212
|
});
|
|
5213
5213
|
|
|
5214
|
-
it('uses
|
|
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('
|
|
5221
|
+
expect(selectedAgent).toBe('default-agent');
|
|
5222
5222
|
});
|
|
5223
5223
|
|
|
5224
|
-
it('
|
|
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('
|
|
5231
|
+
expect(selectedAgent).toBe('default-agent');
|
|
5232
5232
|
});
|
|
5233
5233
|
|
|
5234
|
-
it('
|
|
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('
|
|
5241
|
+
expect(selectedAgent).toBe('default-agent');
|
|
5242
5242
|
});
|
|
5243
5243
|
|
|
5244
|
-
it('
|
|
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-
|
|
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-
|
|
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-
|
|
5281
|
+
expect(selectedAgent).toBe('default-agent');
|
|
5282
5282
|
});
|
|
5283
5283
|
|
|
5284
|
-
it('
|
|
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('
|
|
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
|
-
'
|
|
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 {
|
|
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
|
|
486
|
+
await adoptIssueAgentDesignationLabel(
|
|
524
487
|
issue,
|
|
525
488
|
project,
|
|
526
|
-
params.agents ??
|
|
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;
|
|
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;
|
|
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
|