github-issue-tower-defence-management 1.188.0 → 1.188.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 +8 -0
- package/README.md +3 -0
- package/bin/adapter/entry-points/cli/index.js +20 -12
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -4
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +3 -3
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +13 -1
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
- package/bin/domain/usecases/isHumanComment.js +21 -0
- package/bin/domain/usecases/isHumanComment.js.map +1 -0
- package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js +5 -0
- package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js.map +1 -0
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js +62 -12
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +2 -0
- package/src/adapter/entry-points/cli/index.ts +39 -16
- package/src/adapter/entry-points/cli/projectConfig.ts +1 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +98 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +9 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +9 -4
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +88 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +17 -2
- package/src/domain/usecases/isHumanComment.test.ts +90 -0
- package/src/domain/usecases/isHumanComment.ts +23 -0
- package/src/domain/usecases/nextStepAgentDispatchRepeatedMessage.ts +2 -0
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.test.ts +312 -72
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts +103 -19
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
- package/types/domain/usecases/isHumanComment.d.ts +5 -0
- package/types/domain/usecases/isHumanComment.d.ts.map +1 -0
- package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts +2 -0
- package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts.map +1 -0
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts +5 -2
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts.map +1 -1
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
issueReactivationTriggerStartOfTomorrow,
|
|
36
36
|
} from './issueReactivationTriggerIsPending';
|
|
37
37
|
import { normalizeReportBody } from './normalizeReportBody';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
40
|
+
resolveNextStepAgentDispatchRepetition,
|
|
41
|
+
} from './resolveNextStepAgentDispatchRepetition';
|
|
39
42
|
|
|
40
43
|
export class IssueNotFoundError extends Error {
|
|
41
44
|
constructor(issueUrl: string) {
|
|
@@ -106,6 +109,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
106
109
|
projectUrl: string;
|
|
107
110
|
issueUrl: string;
|
|
108
111
|
thresholdForAutoReject: number;
|
|
112
|
+
thresholdForDispatchLoop?: number;
|
|
109
113
|
workflowBlockerResolvedWebhookUrl: string | null;
|
|
110
114
|
allowedIssueAuthors?: string[] | null;
|
|
111
115
|
labelsAsLlmAgentName?: string[] | null;
|
|
@@ -246,16 +250,16 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
246
250
|
const nextStepAgent = lastAgentReport
|
|
247
251
|
? extractNextStepAgent(lastAgentReport.content)
|
|
248
252
|
: null;
|
|
249
|
-
const commentsAfterLastAgentReport = lastAgentReport
|
|
250
|
-
? comments.slice(comments.indexOf(lastAgentReport) + 1)
|
|
251
|
-
: [];
|
|
252
253
|
if (nextStepAgent !== null) {
|
|
253
254
|
const repetition = resolveNextStepAgentDispatchRepetition({
|
|
254
255
|
agentFieldValue: issue.agent,
|
|
255
256
|
nextStepAgent,
|
|
256
|
-
|
|
257
|
+
comments,
|
|
257
258
|
isTrustedAuthor,
|
|
258
259
|
thresholdForAutoReject: params.thresholdForAutoReject,
|
|
260
|
+
thresholdForDispatchLoop:
|
|
261
|
+
params.thresholdForDispatchLoop ??
|
|
262
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
259
263
|
});
|
|
260
264
|
if (repetition.type === 'escalateToFailedPreparation') {
|
|
261
265
|
issue.status = FAILED_PREPARATION_STATUS_NAME;
|
|
@@ -27,7 +27,10 @@ import { findLastAgentReport } from './findLastAgentReport';
|
|
|
27
27
|
import { isAgentReportBody } from './isAgentReportBody';
|
|
28
28
|
import { ensureAgentOptionAndGetId } from './ensureAgentOptionAndGetId';
|
|
29
29
|
import { normalizeReportBody } from './normalizeReportBody';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
32
|
+
resolveNextStepAgentDispatchRepetition,
|
|
33
|
+
} from './resolveNextStepAgentDispatchRepetition';
|
|
31
34
|
|
|
32
35
|
const ORPHANED_PREPARATION_REJECTION_DETAIL = 'ORPHANED_PREPARATION';
|
|
33
36
|
|
|
@@ -64,6 +67,7 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
64
67
|
projectUrl: string;
|
|
65
68
|
preparationProcessCheckCommand: string;
|
|
66
69
|
thresholdForAutoReject: number;
|
|
70
|
+
thresholdForDispatchLoop?: number;
|
|
67
71
|
awLogDirectoryPath?: string;
|
|
68
72
|
awLogStaleThresholdMinutes?: number;
|
|
69
73
|
awaitingQualityCheckStatus?: string | null;
|
|
@@ -136,15 +140,16 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
136
140
|
const repetition = resolveNextStepAgentDispatchRepetition({
|
|
137
141
|
agentFieldValue: issue.agent,
|
|
138
142
|
nextStepAgent,
|
|
139
|
-
|
|
140
|
-
? comments.slice(comments.indexOf(lastAgentReport) + 1)
|
|
141
|
-
: [],
|
|
143
|
+
comments,
|
|
142
144
|
isTrustedAuthor: (author) =>
|
|
143
145
|
isAuthorAuthorizedForAutoStatusCheck(
|
|
144
146
|
author,
|
|
145
147
|
params.allowedIssueAuthors,
|
|
146
148
|
),
|
|
147
149
|
thresholdForAutoReject: params.thresholdForAutoReject,
|
|
150
|
+
thresholdForDispatchLoop:
|
|
151
|
+
params.thresholdForDispatchLoop ??
|
|
152
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
148
153
|
});
|
|
149
154
|
if (
|
|
150
155
|
repetition.type === 'escalateToFailedPreparation' &&
|
|
@@ -838,6 +838,94 @@ describe('TriagerApprovalDispatchUseCase', () => {
|
|
|
838
838
|
);
|
|
839
839
|
});
|
|
840
840
|
|
|
841
|
+
it('should skip a candidate issue when getCommentsFromIssue throws and continue processing the remaining issues', async () => {
|
|
842
|
+
const failingIssue = createMockIssue({
|
|
843
|
+
number: 1,
|
|
844
|
+
url: 'https://github.com/owner/repo/issues/1',
|
|
845
|
+
itemId: 'item-1',
|
|
846
|
+
status: 'Awaiting Quality Check',
|
|
847
|
+
author: 'owner-user',
|
|
848
|
+
createdAt: new Date('2000-01-01T00:00:00Z'),
|
|
849
|
+
});
|
|
850
|
+
const successIssue = createMockIssue({
|
|
851
|
+
number: 2,
|
|
852
|
+
url: 'https://github.com/owner/repo/issues/2',
|
|
853
|
+
itemId: 'item-2',
|
|
854
|
+
status: 'Awaiting Quality Check',
|
|
855
|
+
author: 'owner-user',
|
|
856
|
+
createdAt: new Date('2000-01-02T00:00:00Z'),
|
|
857
|
+
});
|
|
858
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
859
|
+
project: mockProject,
|
|
860
|
+
issues: [failingIssue, successIssue],
|
|
861
|
+
cacheUsed: false,
|
|
862
|
+
});
|
|
863
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockImplementation(
|
|
864
|
+
async (issue) => {
|
|
865
|
+
if (issue.number === 1) {
|
|
866
|
+
throw new Error(
|
|
867
|
+
'Failed to fetch comments from GitHub REST API: 404 ',
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
return [
|
|
871
|
+
createComment(
|
|
872
|
+
'bot',
|
|
873
|
+
TRIAGER_PROPOSAL_COMMENT(
|
|
874
|
+
'developer',
|
|
875
|
+
'regular / workflow improvement',
|
|
876
|
+
false,
|
|
877
|
+
),
|
|
878
|
+
),
|
|
879
|
+
createComment('owner-user', 'ok'),
|
|
880
|
+
];
|
|
881
|
+
},
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
await useCase.run({
|
|
885
|
+
projectUrl: 'https://github.com/orgs/owner/projects/1',
|
|
886
|
+
allowedIssueAuthors: ['owner-user'],
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
expect(
|
|
890
|
+
mockIssueCommentRepository.getCommentsFromIssue,
|
|
891
|
+
).toHaveBeenCalledTimes(2);
|
|
892
|
+
expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledTimes(1);
|
|
893
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
894
|
+
mockProject,
|
|
895
|
+
successIssue,
|
|
896
|
+
'awaiting-workspace-id',
|
|
897
|
+
);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('should propagate non-404 errors from getCommentsFromIssue', async () => {
|
|
901
|
+
const issue = createMockIssue({
|
|
902
|
+
number: 1,
|
|
903
|
+
url: 'https://github.com/owner/repo/issues/1',
|
|
904
|
+
itemId: 'item-1',
|
|
905
|
+
status: 'Awaiting Quality Check',
|
|
906
|
+
author: 'owner-user',
|
|
907
|
+
createdAt: new Date('2000-01-01T00:00:00Z'),
|
|
908
|
+
});
|
|
909
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
910
|
+
project: mockProject,
|
|
911
|
+
issues: [issue],
|
|
912
|
+
cacheUsed: false,
|
|
913
|
+
});
|
|
914
|
+
const authError = new Error(
|
|
915
|
+
'Failed to fetch comments from GitHub REST API: 401 Unauthorized',
|
|
916
|
+
);
|
|
917
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockRejectedValue(
|
|
918
|
+
authError,
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
await expect(
|
|
922
|
+
useCase.run({
|
|
923
|
+
projectUrl: 'https://github.com/orgs/owner/projects/1',
|
|
924
|
+
allowedIssueAuthors: ['owner-user'],
|
|
925
|
+
}),
|
|
926
|
+
).rejects.toThrow(authError);
|
|
927
|
+
});
|
|
928
|
+
|
|
841
929
|
it('should use the last triager proposal when multiple proposals exist', async () => {
|
|
842
930
|
const issue = createMockIssue({
|
|
843
931
|
status: 'Awaiting Quality Check',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
2
2
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
3
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
|
+
import { Comment } from '../entities/Comment';
|
|
4
5
|
import {
|
|
5
6
|
isAgentReportBody,
|
|
6
7
|
isAgentReportBodyFromAgent,
|
|
@@ -68,6 +69,9 @@ const parseTriagerProposalBlock = (
|
|
|
68
69
|
return null;
|
|
69
70
|
};
|
|
70
71
|
|
|
72
|
+
const isNotFoundError = (error: unknown): boolean =>
|
|
73
|
+
(error instanceof Error ? error.message : String(error)).includes('404');
|
|
74
|
+
|
|
71
75
|
const isApprovalComment = (
|
|
72
76
|
content: string,
|
|
73
77
|
author: string,
|
|
@@ -160,8 +164,19 @@ export class TriagerApprovalDispatchUseCase {
|
|
|
160
164
|
const issue =
|
|
161
165
|
candidateIssues[(windowStart + slot) % candidateIssues.length];
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
let comments: Comment[];
|
|
168
|
+
try {
|
|
169
|
+
comments =
|
|
170
|
+
await this.issueCommentRepository.getCommentsFromIssue(issue);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (!isNotFoundError(error)) {
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
console.warn(
|
|
176
|
+
`[TriagerApprovalDispatch] Failed to fetch comments, skipping issue for this cycle. issueUrl: ${issue.url} error: ${error instanceof Error ? error.message : String(error)}`,
|
|
177
|
+
);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
165
180
|
|
|
166
181
|
let firstProposalIndex = -1;
|
|
167
182
|
let proposal: TriagerProposal | null = null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AUTO_STATUS_CHECK_MESSAGE_HEAD } from './autoStatusCheckComments';
|
|
2
|
+
import { isHumanComment } from './isHumanComment';
|
|
3
|
+
import { NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD } from './nextStepAgentDispatchRepeatedMessage';
|
|
4
|
+
import { RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD } from './returnedToAwaitingWorkspaceMessage';
|
|
5
|
+
|
|
6
|
+
const trustAll = (): boolean => true;
|
|
7
|
+
|
|
8
|
+
describe('isHumanComment', () => {
|
|
9
|
+
it('treats a plain comment from a trusted author as human input', () => {
|
|
10
|
+
expect(
|
|
11
|
+
isHumanComment(
|
|
12
|
+
{ author: 'bot', content: 'Please use the second option.' },
|
|
13
|
+
trustAll,
|
|
14
|
+
),
|
|
15
|
+
).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('treats an agent report as machine generated', () => {
|
|
19
|
+
expect(
|
|
20
|
+
isHumanComment(
|
|
21
|
+
{
|
|
22
|
+
author: 'bot',
|
|
23
|
+
content: 'From: :robot: agent (model)\n\nReport body.',
|
|
24
|
+
},
|
|
25
|
+
trustAll,
|
|
26
|
+
),
|
|
27
|
+
).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('treats an agent report whose leading fenced block precedes the prefix as machine generated', () => {
|
|
31
|
+
expect(
|
|
32
|
+
isHumanComment(
|
|
33
|
+
{
|
|
34
|
+
author: 'bot',
|
|
35
|
+
content:
|
|
36
|
+
'```json\n{ "nextStepAgent": "developer" }\n```\n\nFrom: :robot: agent (model)\n\nReport body.',
|
|
37
|
+
},
|
|
38
|
+
trustAll,
|
|
39
|
+
),
|
|
40
|
+
).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('treats the dispatch repetition comment as machine generated', () => {
|
|
44
|
+
expect(
|
|
45
|
+
isHumanComment(
|
|
46
|
+
{
|
|
47
|
+
author: 'bot',
|
|
48
|
+
content: `${NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} developer\n\nDispatching it again.`,
|
|
49
|
+
},
|
|
50
|
+
trustAll,
|
|
51
|
+
),
|
|
52
|
+
).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('treats an auto status check comment as machine generated', () => {
|
|
56
|
+
expect(
|
|
57
|
+
isHumanComment(
|
|
58
|
+
{
|
|
59
|
+
author: 'bot',
|
|
60
|
+
content: `${AUTO_STATUS_CHECK_MESSAGE_HEAD} PULL_REQUEST_NOT_FOUND`,
|
|
61
|
+
},
|
|
62
|
+
trustAll,
|
|
63
|
+
),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('treats the returned to awaiting workspace comment as machine generated', () => {
|
|
68
|
+
expect(
|
|
69
|
+
isHumanComment(
|
|
70
|
+
{
|
|
71
|
+
author: 'bot',
|
|
72
|
+
content: `${RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD}\nThe last report declared that this task needs no pull request.`,
|
|
73
|
+
},
|
|
74
|
+
trustAll,
|
|
75
|
+
),
|
|
76
|
+
).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('treats any comment from an untrusted author as human input', () => {
|
|
80
|
+
expect(
|
|
81
|
+
isHumanComment(
|
|
82
|
+
{
|
|
83
|
+
author: 'stranger',
|
|
84
|
+
content: 'From: :robot: agent (model)\n\nImpersonated report.',
|
|
85
|
+
},
|
|
86
|
+
(author: string): boolean => author === 'bot',
|
|
87
|
+
),
|
|
88
|
+
).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AUTO_STATUS_CHECK_MESSAGE_HEAD } from './autoStatusCheckComments';
|
|
2
|
+
import { isAgentReportBody } from './isAgentReportBody';
|
|
3
|
+
import { NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD } from './nextStepAgentDispatchRepeatedMessage';
|
|
4
|
+
|
|
5
|
+
const MACHINE_GENERATED_COMMENT_HEADS = [
|
|
6
|
+
AUTO_STATUS_CHECK_MESSAGE_HEAD,
|
|
7
|
+
NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const isHumanComment = (
|
|
11
|
+
comment: { author: string; content: string },
|
|
12
|
+
isTrustedAuthor: (author: string) => boolean,
|
|
13
|
+
): boolean => {
|
|
14
|
+
if (!isTrustedAuthor(comment.author)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (isAgentReportBody(comment.content)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return !MACHINE_GENERATED_COMMENT_HEADS.some((head) =>
|
|
21
|
+
comment.content.startsWith(head),
|
|
22
|
+
);
|
|
23
|
+
};
|