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.
Files changed (54) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +3 -0
  3. package/bin/adapter/entry-points/cli/index.js +20 -12
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  8. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  10. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -4
  11. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  12. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +3 -3
  13. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  14. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +13 -1
  15. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
  16. package/bin/domain/usecases/isHumanComment.js +21 -0
  17. package/bin/domain/usecases/isHumanComment.js.map +1 -0
  18. package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js +5 -0
  19. package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js.map +1 -0
  20. package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js +62 -12
  21. package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/adapter/entry-points/cli/index.test.ts +2 -0
  24. package/src/adapter/entry-points/cli/index.ts +39 -16
  25. package/src/adapter/entry-points/cli/projectConfig.ts +1 -0
  26. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
  27. package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
  28. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +98 -0
  29. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +9 -5
  30. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +9 -4
  31. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +88 -0
  32. package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +17 -2
  33. package/src/domain/usecases/isHumanComment.test.ts +90 -0
  34. package/src/domain/usecases/isHumanComment.ts +23 -0
  35. package/src/domain/usecases/nextStepAgentDispatchRepeatedMessage.ts +2 -0
  36. package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.test.ts +312 -72
  37. package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts +103 -19
  38. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  39. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  40. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  41. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  42. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  43. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  44. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
  45. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  46. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +1 -0
  47. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  48. package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
  49. package/types/domain/usecases/isHumanComment.d.ts +5 -0
  50. package/types/domain/usecases/isHumanComment.d.ts.map +1 -0
  51. package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts +2 -0
  52. package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts.map +1 -0
  53. package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts +5 -2
  54. 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 { resolveNextStepAgentDispatchRepetition } from './resolveNextStepAgentDispatchRepetition';
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
- commentsAfterLastAgentReport,
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 { resolveNextStepAgentDispatchRepetition } from './resolveNextStepAgentDispatchRepetition';
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
- commentsAfterLastAgentReport: lastAgentReport
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
- const comments =
164
- await this.issueCommentRepository.getCommentsFromIssue(issue);
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
+ };
@@ -0,0 +1,2 @@
1
+ export const NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD =
2
+ 'Next step agent dispatch repeated:';