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.
- package/.github/workflows/commit-lint.yml +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/cli/projectConfig.js +6 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +29 -16
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +49 -24
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -1
- package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.js +7 -0
- package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.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/NotifySilentLiveSessionsUseCase.js +33 -2
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter/entry-points/cli/projectConfig.ts +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +5 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +66 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +5 -1
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +183 -18
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +59 -26
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/entities/LiveSessionOutputActivity.ts +1 -0
- package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.test.ts +95 -0
- package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.ts +9 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +70 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +37 -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/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +1 -11
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionOutputActivity.d.ts +1 -0
- package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -1
- package/types/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.d.ts +1 -0
- package/types/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.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/NotifySilentLiveSessionsUseCase.d.ts +1 -0
- 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
|
|
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-
|
|
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:
|
|
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('
|
|
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
|
-
|
|
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-
|
|
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('
|
|
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
|
-
|
|
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:
|
|
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('
|
|
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: '
|
|
309
|
+
sessionName: 'busy',
|
|
155
310
|
lastOutputEpochSeconds: Math.floor(
|
|
156
|
-
Date.parse('2026-06-
|
|
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('
|
|
163
|
-
const transcriptPath = writeTranscript('
|
|
164
|
-
|
|
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([['
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
41
|
-
this.
|
|
42
|
-
if (
|
|
43
|
-
activities.push({
|
|
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
|
|
58
|
+
private readTranscriptActivity = (
|
|
50
59
|
transcriptPath: string,
|
|
51
|
-
):
|
|
60
|
+
): TranscriptActivity => {
|
|
52
61
|
let content: string;
|
|
53
62
|
try {
|
|
54
63
|
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
55
64
|
} catch {
|
|
56
|
-
return
|
|
65
|
+
return {
|
|
66
|
+
lastAssistantOutputEpochSeconds: null,
|
|
67
|
+
hasInProgressToolCall: false,
|
|
68
|
+
};
|
|
57
69
|
}
|
|
58
|
-
let
|
|
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
|
|
74
|
-
if (
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
};
|
|
@@ -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,
|