github-issue-tower-defence-management 1.126.2 → 1.127.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.github/workflows/commit-lint.yml +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +2 -0
  4. package/bin/adapter/entry-points/cli/projectConfig.js +6 -0
  5. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +29 -16
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  8. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +49 -24
  9. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -1
  10. package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.js +7 -0
  11. package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.js.map +1 -1
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  13. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  14. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +33 -2
  15. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  16. package/package.json +4 -4
  17. package/src/adapter/entry-points/cli/projectConfig.ts +14 -0
  18. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +5 -2
  19. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +66 -0
  20. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +5 -1
  21. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +183 -18
  22. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +59 -26
  23. package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
  24. package/src/domain/entities/LiveSessionOutputActivity.ts +1 -0
  25. package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.test.ts +95 -0
  26. package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.ts +9 -0
  27. package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
  28. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +70 -0
  29. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +37 -1
  30. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  31. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  32. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  33. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +1 -11
  34. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -1
  35. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
  36. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
  37. package/types/domain/entities/LiveSessionOutputActivity.d.ts +1 -0
  38. package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -1
  39. package/types/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.d.ts +1 -0
  40. package/types/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.d.ts.map +1 -1
  41. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  42. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  43. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +1 -0
  44. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
@@ -25,6 +25,14 @@ describe('FileSystemSessionOutputActivityRepository', () => {
25
25
  },
26
26
  });
27
27
 
28
+ const assistantEntryWithoutTimestamp = (): object => ({
29
+ type: 'assistant',
30
+ message: {
31
+ role: 'assistant',
32
+ content: [{ type: 'text', text: 'no timestamp on this entry' }],
33
+ },
34
+ });
35
+
28
36
  const userEntry = (timestamp: string): object => ({
29
37
  type: 'user',
30
38
  timestamp,
@@ -37,6 +45,51 @@ describe('FileSystemSessionOutputActivityRepository', () => {
37
45
  toolUseResult: { stdout: 'done', stderr: '' },
38
46
  });
39
47
 
48
+ const systemEntry = (timestamp: string): object => ({
49
+ type: 'system',
50
+ timestamp,
51
+ content: 'Compacting conversation history',
52
+ });
53
+
54
+ const assistantEntryWithToolUse = (
55
+ timestamp: string,
56
+ toolUseId: string,
57
+ ): object => ({
58
+ type: 'assistant',
59
+ timestamp,
60
+ message: {
61
+ role: 'assistant',
62
+ stop_reason: 'tool_use',
63
+ content: [
64
+ { type: 'text', text: 'running a command' },
65
+ {
66
+ type: 'tool_use',
67
+ id: toolUseId,
68
+ name: 'Bash',
69
+ input: { command: 'sleep 590' },
70
+ },
71
+ ],
72
+ },
73
+ });
74
+
75
+ const toolResultUserEntry = (
76
+ timestamp: string,
77
+ toolUseId: string,
78
+ ): object => ({
79
+ type: 'user',
80
+ timestamp,
81
+ message: {
82
+ role: 'user',
83
+ content: [
84
+ {
85
+ type: 'tool_result',
86
+ tool_use_id: toolUseId,
87
+ content: 'done',
88
+ },
89
+ ],
90
+ },
91
+ });
92
+
40
93
  const untimestampedEntry = (): object => ({
41
94
  type: 'summary',
42
95
  summary: 'no timestamp on this entry',
@@ -52,11 +105,56 @@ describe('FileSystemSessionOutputActivityRepository', () => {
52
105
  return filePath;
53
106
  };
54
107
 
55
- it('returns the latest entry timestamp of any type as the last activity epoch', async () => {
108
+ it('returns the old last-assistant timestamp when recent system and tool entries follow it', async () => {
109
+ const transcriptPath = writeTranscript('thrashing.jsonl', [
110
+ assistantEntry('2026-06-27T01:00:00.000Z'),
111
+ systemEntry('2026-06-27T09:30:00.000Z'),
112
+ toolResultEntry('2026-06-27T09:45:00.000Z'),
113
+ systemEntry('2026-06-27T10:00:00.000Z'),
114
+ ]);
115
+ const repository = new FileSystemSessionOutputActivityRepository();
116
+
117
+ const result = await repository.listSessionOutputActivities(
118
+ new Map([['thrashing', transcriptPath]]),
119
+ );
120
+
121
+ expect(result).toEqual([
122
+ {
123
+ sessionName: 'thrashing',
124
+ lastOutputEpochSeconds: Math.floor(
125
+ Date.parse('2026-06-27T01:00:00.000Z') / 1000,
126
+ ),
127
+ hasInProgressToolCall: false,
128
+ },
129
+ ]);
130
+ });
131
+
132
+ it('returns the recent timestamp when the latest assistant entry is recent', async () => {
133
+ const transcriptPath = writeTranscript('working.jsonl', [
134
+ userEntry('2026-06-27T09:00:00.000Z'),
135
+ assistantEntry('2026-06-27T09:55:00.000Z'),
136
+ ]);
137
+ const repository = new FileSystemSessionOutputActivityRepository();
138
+
139
+ const result = await repository.listSessionOutputActivities(
140
+ new Map([['working', transcriptPath]]),
141
+ );
142
+
143
+ expect(result).toEqual([
144
+ {
145
+ sessionName: 'working',
146
+ lastOutputEpochSeconds: Math.floor(
147
+ Date.parse('2026-06-27T09:55:00.000Z') / 1000,
148
+ ),
149
+ hasInProgressToolCall: false,
150
+ },
151
+ ]);
152
+ });
153
+
154
+ it('ignores a later user entry and returns the last assistant timestamp', async () => {
56
155
  const transcriptPath = writeTranscript('workbench.jsonl', [
57
156
  assistantEntry('2026-06-27T10:00:00.000Z'),
58
- userEntry('2026-06-27T10:30:00.000Z'),
59
- assistantEntry('2026-06-27T10:05:00.000Z'),
157
+ userEntry('2026-06-27T11:00:00.000Z'),
60
158
  ]);
61
159
  const repository = new FileSystemSessionOutputActivityRepository();
62
160
 
@@ -68,16 +166,41 @@ describe('FileSystemSessionOutputActivityRepository', () => {
68
166
  {
69
167
  sessionName: 'workbench',
70
168
  lastOutputEpochSeconds: Math.floor(
71
- Date.parse('2026-06-27T10:30:00.000Z') / 1000,
169
+ Date.parse('2026-06-27T10:00:00.000Z') / 1000,
72
170
  ),
171
+ hasInProgressToolCall: false,
73
172
  },
74
173
  ]);
75
174
  });
76
175
 
77
- it('advances the last activity time when a later user entry follows an assistant entry', async () => {
176
+ it('ignores a later tool_result entry and returns the last assistant timestamp', async () => {
78
177
  const transcriptPath = writeTranscript('workbench.jsonl', [
79
178
  assistantEntry('2026-06-27T10:00:00.000Z'),
80
- userEntry('2026-06-27T11:00:00.000Z'),
179
+ toolResultEntry('2026-06-27T10:45:00.000Z'),
180
+ ]);
181
+ const repository = new FileSystemSessionOutputActivityRepository();
182
+
183
+ const result = await repository.listSessionOutputActivities(
184
+ new Map([['workbench', transcriptPath]]),
185
+ );
186
+
187
+ expect(result).toEqual([
188
+ {
189
+ sessionName: 'workbench',
190
+ lastOutputEpochSeconds: Math.floor(
191
+ Date.parse('2026-06-27T10:00:00.000Z') / 1000,
192
+ ),
193
+ hasInProgressToolCall: false,
194
+ },
195
+ ]);
196
+ });
197
+
198
+ it('returns the last assistant entry timestamp when several entry types interleave', async () => {
199
+ const transcriptPath = writeTranscript('workbench.jsonl', [
200
+ assistantEntry('2026-06-27T10:00:00.000Z'),
201
+ userEntry('2026-06-27T10:10:00.000Z'),
202
+ assistantEntry('2026-06-27T10:05:00.000Z'),
203
+ systemEntry('2026-06-27T10:40:00.000Z'),
81
204
  ]);
82
205
  const repository = new FileSystemSessionOutputActivityRepository();
83
206
 
@@ -89,16 +212,17 @@ describe('FileSystemSessionOutputActivityRepository', () => {
89
212
  {
90
213
  sessionName: 'workbench',
91
214
  lastOutputEpochSeconds: Math.floor(
92
- Date.parse('2026-06-27T11:00:00.000Z') / 1000,
215
+ Date.parse('2026-06-27T10:05:00.000Z') / 1000,
93
216
  ),
217
+ hasInProgressToolCall: false,
94
218
  },
95
219
  ]);
96
220
  });
97
221
 
98
- it('advances the last activity time when a tool_result entry is the latest entry', async () => {
222
+ it('falls back to the last assistant entry that has a parseable timestamp', async () => {
99
223
  const transcriptPath = writeTranscript('workbench.jsonl', [
100
224
  assistantEntry('2026-06-27T10:00:00.000Z'),
101
- toolResultEntry('2026-06-27T10:45:00.000Z'),
225
+ assistantEntryWithoutTimestamp(),
102
226
  ]);
103
227
  const repository = new FileSystemSessionOutputActivityRepository();
104
228
 
@@ -110,8 +234,9 @@ describe('FileSystemSessionOutputActivityRepository', () => {
110
234
  {
111
235
  sessionName: 'workbench',
112
236
  lastOutputEpochSeconds: Math.floor(
113
- Date.parse('2026-06-27T10:45:00.000Z') / 1000,
237
+ Date.parse('2026-06-27T10:00:00.000Z') / 1000,
114
238
  ),
239
+ hasInProgressToolCall: false,
115
240
  },
116
241
  ]);
117
242
  });
@@ -135,13 +260,29 @@ describe('FileSystemSessionOutputActivityRepository', () => {
135
260
  lastOutputEpochSeconds: Math.floor(
136
261
  Date.parse('2026-06-27T10:00:00.000Z') / 1000,
137
262
  ),
263
+ hasInProgressToolCall: false,
138
264
  },
139
265
  ]);
140
266
  });
141
267
 
142
- it('resolves a transcript whose only entry is a non-assistant entry', async () => {
268
+ it('omits a transcript whose only entries are non-assistant entries', async () => {
143
269
  const transcriptPath = writeTranscript('workbench.jsonl', [
144
270
  userEntry('2026-06-27T10:00:00.000Z'),
271
+ toolResultEntry('2026-06-27T10:05:00.000Z'),
272
+ systemEntry('2026-06-27T10:10:00.000Z'),
273
+ ]);
274
+ const repository = new FileSystemSessionOutputActivityRepository();
275
+
276
+ const result = await repository.listSessionOutputActivities(
277
+ new Map([['workbench', transcriptPath]]),
278
+ );
279
+
280
+ expect(result).toEqual([]);
281
+ });
282
+
283
+ it('omits sessions whose transcript has no parseable timestamp', async () => {
284
+ const transcriptPath = writeTranscript('workbench.jsonl', [
285
+ untimestampedEntry(),
145
286
  ]);
146
287
  const repository = new FileSystemSessionOutputActivityRepository();
147
288
 
@@ -149,27 +290,51 @@ describe('FileSystemSessionOutputActivityRepository', () => {
149
290
  new Map([['workbench', transcriptPath]]),
150
291
  );
151
292
 
293
+ expect(result).toEqual([]);
294
+ });
295
+
296
+ it('flags a session as waiting on a running tool when the last assistant tool_use has no matching tool_result', async () => {
297
+ const transcriptPath = writeTranscript('busy.jsonl', [
298
+ assistantEntry('2026-06-27T09:00:00.000Z'),
299
+ assistantEntryWithToolUse('2026-06-27T09:01:00.000Z', 'toolu_running'),
300
+ ]);
301
+ const repository = new FileSystemSessionOutputActivityRepository();
302
+
303
+ const result = await repository.listSessionOutputActivities(
304
+ new Map([['busy', transcriptPath]]),
305
+ );
306
+
152
307
  expect(result).toEqual([
153
308
  {
154
- sessionName: 'workbench',
309
+ sessionName: 'busy',
155
310
  lastOutputEpochSeconds: Math.floor(
156
- Date.parse('2026-06-27T10:00:00.000Z') / 1000,
311
+ Date.parse('2026-06-27T09:01:00.000Z') / 1000,
157
312
  ),
313
+ hasInProgressToolCall: true,
158
314
  },
159
315
  ]);
160
316
  });
161
317
 
162
- it('omits sessions whose transcript has no parseable timestamp', async () => {
163
- const transcriptPath = writeTranscript('workbench.jsonl', [
164
- untimestampedEntry(),
318
+ it('does not flag a session once the matching tool_result for its last tool_use is appended', async () => {
319
+ const transcriptPath = writeTranscript('completed.jsonl', [
320
+ assistantEntryWithToolUse('2026-06-27T09:01:00.000Z', 'toolu_done'),
321
+ toolResultUserEntry('2026-06-27T09:05:00.000Z', 'toolu_done'),
165
322
  ]);
166
323
  const repository = new FileSystemSessionOutputActivityRepository();
167
324
 
168
325
  const result = await repository.listSessionOutputActivities(
169
- new Map([['workbench', transcriptPath]]),
326
+ new Map([['completed', transcriptPath]]),
170
327
  );
171
328
 
172
- expect(result).toEqual([]);
329
+ expect(result).toEqual([
330
+ {
331
+ sessionName: 'completed',
332
+ lastOutputEpochSeconds: Math.floor(
333
+ Date.parse('2026-06-27T09:01:00.000Z') / 1000,
334
+ ),
335
+ hasInProgressToolCall: false,
336
+ },
337
+ ]);
173
338
  });
174
339
 
175
340
  it('returns an empty list when no sessions are requested', async () => {
@@ -21,41 +21,54 @@ const parseEpochMilliseconds = (timestamp: string | null): number | null => {
21
21
  return Number.isNaN(parsed) ? null : parsed;
22
22
  };
23
23
 
24
- /**
25
- * Reads the last main-session activity time for each live session from its
26
- * already-resolved transcript path. Idle time is computed from the timestamp of
27
- * the latest entry of any kind (assistant text, owner replies, tool results, or
28
- * any other entry type) rather than from the transcript file modification time.
29
- * Because a session that is actively running tool calls keeps appending entries
30
- * such as `user` and `tool_result` even while it emits no assistant text, every
31
- * entry with a parseable timestamp counts as activity, so a working session is
32
- * not mistaken for a silent one.
33
- */
24
+ const readContentBlocks = (
25
+ message: Record<string, unknown>,
26
+ ): Record<string, unknown>[] => {
27
+ const content = message.content;
28
+ if (!Array.isArray(content)) {
29
+ return [];
30
+ }
31
+ return content.filter(isRecord);
32
+ };
33
+
34
+ type TranscriptActivity = {
35
+ lastAssistantOutputEpochSeconds: number | null;
36
+ hasInProgressToolCall: boolean;
37
+ };
38
+
34
39
  export class FileSystemSessionOutputActivityRepository implements SessionOutputActivityRepository {
35
40
  listSessionOutputActivities = async (
36
41
  transcriptPathBySessionName: Map<string, string>,
37
42
  ): Promise<LiveSessionOutputActivity[]> => {
38
43
  const activities: LiveSessionOutputActivity[] = [];
39
44
  for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
40
- const lastOutputEpochSeconds =
41
- this.readLastActivityEpochSeconds(transcriptPath);
42
- if (lastOutputEpochSeconds !== null) {
43
- activities.push({ sessionName, lastOutputEpochSeconds });
45
+ const { lastAssistantOutputEpochSeconds, hasInProgressToolCall } =
46
+ this.readTranscriptActivity(transcriptPath);
47
+ if (lastAssistantOutputEpochSeconds !== null) {
48
+ activities.push({
49
+ sessionName,
50
+ lastOutputEpochSeconds: lastAssistantOutputEpochSeconds,
51
+ hasInProgressToolCall,
52
+ });
44
53
  }
45
54
  }
46
55
  return activities;
47
56
  };
48
57
 
49
- private readLastActivityEpochSeconds = (
58
+ private readTranscriptActivity = (
50
59
  transcriptPath: string,
51
- ): number | null => {
60
+ ): TranscriptActivity => {
52
61
  let content: string;
53
62
  try {
54
63
  content = fs.readFileSync(transcriptPath, 'utf8');
55
64
  } catch {
56
- return null;
65
+ return {
66
+ lastAssistantOutputEpochSeconds: null,
67
+ hasInProgressToolCall: false,
68
+ };
57
69
  }
58
- let lastActivityEpochMs: number | null = null;
70
+ let lastAssistantOutputEpochMs: number | null = null;
71
+ const pendingToolUseIds = new Set<string>();
59
72
  for (const line of content.split('\n')) {
60
73
  const trimmed = line.trim();
61
74
  if (trimmed.length === 0) {
@@ -70,17 +83,37 @@ export class FileSystemSessionOutputActivityRepository implements SessionOutputA
70
83
  if (!isRecord(parsed)) {
71
84
  continue;
72
85
  }
73
- const epochMs = parseEpochMilliseconds(readString(parsed, 'timestamp'));
74
- if (epochMs === null) {
86
+ const message = parsed.message;
87
+ if (readString(parsed, 'type') === 'assistant') {
88
+ const epochMs = parseEpochMilliseconds(readString(parsed, 'timestamp'));
89
+ if (epochMs !== null) {
90
+ lastAssistantOutputEpochMs = epochMs;
91
+ }
92
+ }
93
+ if (!isRecord(message)) {
75
94
  continue;
76
95
  }
77
- if (lastActivityEpochMs === null || epochMs > lastActivityEpochMs) {
78
- lastActivityEpochMs = epochMs;
96
+ for (const block of readContentBlocks(message)) {
97
+ const blockType = readString(block, 'type');
98
+ if (blockType === 'tool_use') {
99
+ const toolUseId = readString(block, 'id');
100
+ if (toolUseId !== null) {
101
+ pendingToolUseIds.add(toolUseId);
102
+ }
103
+ } else if (blockType === 'tool_result') {
104
+ const toolUseId = readString(block, 'tool_use_id');
105
+ if (toolUseId !== null) {
106
+ pendingToolUseIds.delete(toolUseId);
107
+ }
108
+ }
79
109
  }
80
110
  }
81
- if (lastActivityEpochMs === null) {
82
- return null;
83
- }
84
- return Math.floor(lastActivityEpochMs / 1000);
111
+ return {
112
+ lastAssistantOutputEpochSeconds:
113
+ lastAssistantOutputEpochMs === null
114
+ ? null
115
+ : Math.floor(lastAssistantOutputEpochMs / 1000),
116
+ hasInProgressToolCall: pendingToolUseIds.size > 0,
117
+ };
85
118
  };
86
119
  }
@@ -8,6 +8,7 @@ export type SubAgentActivity = {
8
8
  export type LiveSessionActivitySnapshot = {
9
9
  sessionName: string;
10
10
  mainSilentSeconds: number | null;
11
+ mainHasInProgressToolCall: boolean;
11
12
  subAgents: SubAgentActivity[];
12
13
  unansweredOwnerCallAgeSeconds: number | null;
13
14
  };
@@ -1,4 +1,5 @@
1
1
  export type LiveSessionOutputActivity = {
2
2
  sessionName: string;
3
3
  lastOutputEpochSeconds: number;
4
+ hasInProgressToolCall: boolean;
4
5
  };
@@ -180,6 +180,101 @@ describe('AssignNoAssigneeIssueToManagerUseCase', () => {
180
180
  consoleErrorSpy.mockRestore();
181
181
  });
182
182
 
183
+ it('should assign only issues whose author is in autoAssignManagerAuthors', async () => {
184
+ const allowedIssue = {
185
+ ...basicIssue,
186
+ author: 'renovate[bot]',
187
+ };
188
+ const disallowedIssue = {
189
+ ...basicIssue,
190
+ author: 'human-author',
191
+ };
192
+
193
+ await useCase.run({
194
+ issues: [allowedIssue, disallowedIssue],
195
+ manager: 'manager1',
196
+ cacheUsed: false,
197
+ autoAssignManagerAuthors: ['renovate[bot]'],
198
+ });
199
+
200
+ expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
201
+ [allowedIssue, ['manager1']],
202
+ ]);
203
+ });
204
+
205
+ it('should assign only listed authors in a mixed-author set', async () => {
206
+ const renovateIssue = {
207
+ ...basicIssue,
208
+ author: 'renovate[bot]',
209
+ };
210
+ const humanIssue = {
211
+ ...basicIssue,
212
+ author: 'human-author',
213
+ };
214
+ const dependabotIssue = {
215
+ ...basicIssue,
216
+ author: 'dependabot[bot]',
217
+ };
218
+
219
+ await useCase.run({
220
+ issues: [renovateIssue, humanIssue, dependabotIssue],
221
+ manager: 'manager1',
222
+ cacheUsed: false,
223
+ autoAssignManagerAuthors: ['renovate[bot]', 'dependabot[bot]'],
224
+ });
225
+
226
+ expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
227
+ [renovateIssue, ['manager1']],
228
+ [dependabotIssue, ['manager1']],
229
+ ]);
230
+ });
231
+
232
+ it('should assign all unassigned issues when autoAssignManagerAuthors is null', async () => {
233
+ const firstIssue = {
234
+ ...basicIssue,
235
+ author: 'renovate[bot]',
236
+ };
237
+ const secondIssue = {
238
+ ...basicIssue,
239
+ author: 'human-author',
240
+ };
241
+
242
+ await useCase.run({
243
+ issues: [firstIssue, secondIssue],
244
+ manager: 'manager1',
245
+ cacheUsed: false,
246
+ autoAssignManagerAuthors: null,
247
+ });
248
+
249
+ expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
250
+ [firstIssue, ['manager1']],
251
+ [secondIssue, ['manager1']],
252
+ ]);
253
+ });
254
+
255
+ it('should assign all unassigned issues when autoAssignManagerAuthors is an empty array', async () => {
256
+ const firstIssue = {
257
+ ...basicIssue,
258
+ author: 'renovate[bot]',
259
+ };
260
+ const secondIssue = {
261
+ ...basicIssue,
262
+ author: 'human-author',
263
+ };
264
+
265
+ await useCase.run({
266
+ issues: [firstIssue, secondIssue],
267
+ manager: 'manager1',
268
+ cacheUsed: false,
269
+ autoAssignManagerAuthors: [],
270
+ });
271
+
272
+ expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
273
+ [firstIssue, ['manager1']],
274
+ [secondIssue, ['manager1']],
275
+ ]);
276
+ });
277
+
183
278
  it('should rethrow non-Error thrown values without logging', async () => {
184
279
  const failingIssue = {
185
280
  ...basicIssue,
@@ -11,14 +11,23 @@ export class AssignNoAssigneeIssueToManagerUseCase {
11
11
  issues: Issue[];
12
12
  manager: Member['name'];
13
13
  cacheUsed: boolean;
14
+ autoAssignManagerAuthors?: string[] | null;
14
15
  }): Promise<void> => {
15
16
  if (input.cacheUsed) {
16
17
  return;
17
18
  }
19
+ const authorAllowList =
20
+ input.autoAssignManagerAuthors &&
21
+ input.autoAssignManagerAuthors.length > 0
22
+ ? input.autoAssignManagerAuthors
23
+ : null;
18
24
  for (const issue of input.issues) {
19
25
  if (issue.assignees.length > 0 || issue.state !== 'OPEN') {
20
26
  continue;
21
27
  }
28
+ if (authorAllowList !== null && !authorAllowList.includes(issue.author)) {
29
+ continue;
30
+ }
22
31
  try {
23
32
  await this.issueRepository.updateAssigneeList(issue, [input.manager]);
24
33
  } catch (e) {
@@ -154,6 +154,7 @@ export class HandleScheduledEventUseCase {
154
154
  labelsAsLlmAgentName?: string[] | null;
155
155
  changeTargetPathAliases?: Record<string, string> | null;
156
156
  allowedIssueAuthors?: string[] | null;
157
+ autoAssignManagerAuthors?: string[] | null;
157
158
  startPreparation?: {
158
159
  defaultAgentName: string;
159
160
  defaultLlmModelName?: string | null;
@@ -549,6 +550,7 @@ ${JSON.stringify(e)}
549
550
  issues,
550
551
  manager: input.manager,
551
552
  cacheUsed,
553
+ autoAssignManagerAuthors: input.autoAssignManagerAuthors ?? null,
552
554
  });
553
555
  await this.updateIssueStatusByLabelUseCase.run({
554
556
  project,