github-issue-tower-defence-management 1.150.0 → 1.151.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 (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -1
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +24 -0
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js +13 -1
  6. package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/resolveUnansweredOwnerCalls.js +12 -0
  8. package/bin/adapter/entry-points/handlers/resolveUnansweredOwnerCalls.js.map +1 -0
  9. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +60 -13
  10. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
  11. package/bin/domain/entities/UnansweredOwnerCall.js +3 -0
  12. package/bin/domain/entities/UnansweredOwnerCall.js.map +1 -0
  13. package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js +21 -2
  14. package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +46 -0
  17. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +104 -1
  18. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts +19 -0
  19. package/src/adapter/entry-points/handlers/resolveUnansweredOwnerCalls.test.ts +111 -0
  20. package/src/adapter/entry-points/handlers/resolveUnansweredOwnerCalls.ts +33 -0
  21. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +178 -0
  22. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +106 -17
  23. package/src/domain/entities/UnansweredOwnerCall.ts +4 -0
  24. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +99 -0
  25. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.ts +47 -1
  26. package/src/domain/usecases/intmux/UnansweredOwnerCallSessionKeyContract.test.ts +141 -0
  27. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  28. package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts +2 -0
  29. package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts.map +1 -1
  30. package/types/adapter/entry-points/handlers/resolveUnansweredOwnerCalls.d.ts +13 -0
  31. package/types/adapter/entry-points/handlers/resolveUnansweredOwnerCalls.d.ts.map +1 -0
  32. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +5 -0
  33. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
  34. package/types/domain/entities/UnansweredOwnerCall.d.ts +5 -0
  35. package/types/domain/entities/UnansweredOwnerCall.d.ts.map +1 -0
  36. package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts +19 -0
  37. package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts.map +1 -1
@@ -1,8 +1,20 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
4
+ import { UnansweredOwnerCall } from '../../domain/entities/UnansweredOwnerCall';
4
5
  import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSessionReminderSentinel';
5
6
 
7
+ type TranscriptOwnerCall = {
8
+ epochMs: number;
9
+ body: string;
10
+ candidateOnly: boolean;
11
+ };
12
+
13
+ type TranscriptOwnerCallScan = {
14
+ ownerCalls: TranscriptOwnerCall[];
15
+ lastOwnerReplyEpochMs: number | null;
16
+ };
17
+
6
18
  const isRecord = (value: unknown): value is Record<string, unknown> =>
7
19
  typeof value === 'object' && value !== null;
8
20
 
@@ -181,18 +193,58 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
181
193
  return unansweredOwnerCallEpochSecondsBySessionName;
182
194
  };
183
195
 
184
- private findUnansweredOwnerCallEpochMs = (
196
+ listUnansweredOwnerCallsBySessionName = async (
197
+ transcriptPathBySessionName: Map<string, string>,
198
+ ): Promise<Map<string, UnansweredOwnerCall[]>> => {
199
+ const unansweredOwnerCallsBySessionName = new Map<
200
+ string,
201
+ UnansweredOwnerCall[]
202
+ >();
203
+ if (this.ownerCallMarkerFamily.length === 0) {
204
+ return unansweredOwnerCallsBySessionName;
205
+ }
206
+ for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
207
+ const transcript = this.scanTranscript(
208
+ transcriptPath,
209
+ this.ownerCallMarkerFamily,
210
+ );
211
+ if (transcript === null) {
212
+ continue;
213
+ }
214
+ const answeredBeforeEpochMs = this.resolveReplyEpochMs(
215
+ transcriptPath,
216
+ transcript.lastOwnerReplyEpochMs,
217
+ );
218
+ const unansweredCalls = transcript.ownerCalls
219
+ .filter(
220
+ (ownerCall) =>
221
+ (answeredBeforeEpochMs === null ||
222
+ ownerCall.epochMs > answeredBeforeEpochMs) &&
223
+ this.isCallDeliveredToOwner(transcriptPath, ownerCall),
224
+ )
225
+ .sort((oneCall, otherCall) => oneCall.epochMs - otherCall.epochMs)
226
+ .map((ownerCall) => ({
227
+ calledAt: new Date(ownerCall.epochMs).toISOString(),
228
+ body: ownerCall.body,
229
+ }));
230
+ if (unansweredCalls.length > 0) {
231
+ unansweredOwnerCallsBySessionName.set(sessionName, unansweredCalls);
232
+ }
233
+ }
234
+ return unansweredOwnerCallsBySessionName;
235
+ };
236
+
237
+ private scanTranscript = (
185
238
  transcriptPath: string,
186
239
  markerFamily: string[],
187
- ): number | null => {
240
+ ): TranscriptOwnerCallScan | null => {
188
241
  let content: string;
189
242
  try {
190
243
  content = fs.readFileSync(transcriptPath, 'utf8');
191
244
  } catch {
192
245
  return null;
193
246
  }
194
- let lastOwnerCallEpochMs: number | null = null;
195
- let lastOwnerCallIsCandidateOnly = false;
247
+ const ownerCalls: TranscriptOwnerCall[] = [];
196
248
  let lastOwnerReplyEpochMs: number | null = null;
197
249
  for (const line of content.split('\n')) {
198
250
  const trimmed = line.trim();
@@ -221,10 +273,11 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
221
273
  assistantText.includes(marker),
222
274
  );
223
275
  if (matchedMarkers.length > 0) {
224
- lastOwnerCallEpochMs = epochMs;
225
- lastOwnerCallIsCandidateOnly = matchedMarkers.every(
226
- isCandidateOwnerCallMarker,
227
- );
276
+ ownerCalls.push({
277
+ epochMs,
278
+ body: assistantText,
279
+ candidateOnly: matchedMarkers.every(isCandidateOwnerCallMarker),
280
+ });
228
281
  }
229
282
  }
230
283
  if (
@@ -241,27 +294,50 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
241
294
  : lastOwnerReplyEpochMs;
242
295
  }
243
296
  }
244
- if (lastOwnerCallEpochMs === null) {
297
+ return { ownerCalls, lastOwnerReplyEpochMs };
298
+ };
299
+
300
+ private resolveReplyEpochMs = (
301
+ transcriptPath: string,
302
+ lastOwnerReplyEpochMs: number | null,
303
+ ): number | null => {
304
+ const markerReplyEpochMs = this.readOwnerReplyMarkerEpochMs(transcriptPath);
305
+ return markerReplyEpochMs !== null &&
306
+ (lastOwnerReplyEpochMs === null ||
307
+ markerReplyEpochMs > lastOwnerReplyEpochMs)
308
+ ? markerReplyEpochMs
309
+ : lastOwnerReplyEpochMs;
310
+ };
311
+
312
+ private findUnansweredOwnerCallEpochMs = (
313
+ transcriptPath: string,
314
+ markerFamily: string[],
315
+ ): number | null => {
316
+ const transcript = this.scanTranscript(transcriptPath, markerFamily);
317
+ if (transcript === null) {
245
318
  return null;
246
319
  }
320
+ const lastOwnerCall =
321
+ transcript.ownerCalls[transcript.ownerCalls.length - 1] ?? null;
322
+ if (lastOwnerCall === null) {
323
+ return null;
324
+ }
325
+ const lastOwnerCallEpochMs = lastOwnerCall.epochMs;
247
326
  if (
248
327
  this.isCallSuppressedUndelivered(transcriptPath, lastOwnerCallEpochMs)
249
328
  ) {
250
329
  return null;
251
330
  }
252
331
  if (
253
- lastOwnerCallIsCandidateOnly &&
332
+ lastOwnerCall.candidateOnly &&
254
333
  !this.isCandidateCallDelivered(transcriptPath, lastOwnerCallEpochMs)
255
334
  ) {
256
335
  return null;
257
336
  }
258
- const markerReplyEpochMs = this.readOwnerReplyMarkerEpochMs(transcriptPath);
259
- const resolvedReplyEpochMs =
260
- markerReplyEpochMs !== null &&
261
- (lastOwnerReplyEpochMs === null ||
262
- markerReplyEpochMs > lastOwnerReplyEpochMs)
263
- ? markerReplyEpochMs
264
- : lastOwnerReplyEpochMs;
337
+ const resolvedReplyEpochMs = this.resolveReplyEpochMs(
338
+ transcriptPath,
339
+ transcript.lastOwnerReplyEpochMs,
340
+ );
265
341
  return resolvedReplyEpochMs === null ||
266
342
  lastOwnerCallEpochMs > resolvedReplyEpochMs
267
343
  ? lastOwnerCallEpochMs
@@ -285,6 +361,19 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
285
361
  // on the owner would silence the session's stall reminder for good — the session would keep its
286
362
  // task and never be woken again. Such a call is therefore not an outstanding owner call here. A
287
363
  // delivered call, and a newer call the suppression marker does not name, are untouched.
364
+ private isCallDeliveredToOwner = (
365
+ transcriptPath: string,
366
+ ownerCall: TranscriptOwnerCall,
367
+ ): boolean => {
368
+ if (this.isCallSuppressedUndelivered(transcriptPath, ownerCall.epochMs)) {
369
+ return false;
370
+ }
371
+ return (
372
+ !ownerCall.candidateOnly ||
373
+ this.isCandidateCallDelivered(transcriptPath, ownerCall.epochMs)
374
+ );
375
+ };
376
+
288
377
  private isCallSuppressedUndelivered = (
289
378
  transcriptPath: string,
290
379
  ownerCallEpochMs: number,
@@ -0,0 +1,4 @@
1
+ export type UnansweredOwnerCall = {
2
+ calledAt: string;
3
+ body: string;
4
+ };
@@ -1,6 +1,8 @@
1
1
  import { Issue } from '../../entities/Issue';
2
2
  import { FieldOption, Project } from '../../entities/Project';
3
3
  import { GenerateInTmuxByHumanDataUseCase } from './GenerateInTmuxByHumanDataUseCase';
4
+ import { UnansweredOwnerCall } from '../../entities/UnansweredOwnerCall';
5
+ import { toTmuxSessionName } from './InTmuxByHumanSessionReconcileUseCase';
4
6
 
5
7
  const ASSIGNEE = 'owner-login';
6
8
  const CONSOLE_BASE_URL = 'https://console.example.test';
@@ -98,6 +100,7 @@ describe('GenerateInTmuxByHumanDataUseCase', () => {
98
100
  consoleBaseUrl?: string | null;
99
101
  consoleToken?: string | null;
100
102
  newIssueRepo?: string;
103
+ unansweredCallsByTmuxSessionName?: Map<string, UnansweredOwnerCall[]>;
101
104
  } = {},
102
105
  ) =>
103
106
  usecase.run({
@@ -116,6 +119,9 @@ describe('GenerateInTmuxByHumanDataUseCase', () => {
116
119
  overrides.consoleToken === undefined
117
120
  ? CONSOLE_TOKEN
118
121
  : overrides.consoleToken,
122
+ unansweredCallsByTmuxSessionName:
123
+ overrides.unansweredCallsByTmuxSessionName ??
124
+ new Map<string, UnansweredOwnerCall[]>(),
119
125
  now: NOW,
120
126
  });
121
127
 
@@ -333,4 +339,97 @@ describe('GenerateInTmuxByHumanDataUseCase', () => {
333
339
  expect(result.v3).not.toBeNull();
334
340
  });
335
341
  });
342
+
343
+ describe('v5 document', () => {
344
+ const callsFor = (
345
+ issueUrl: string,
346
+ calls: UnansweredOwnerCall[],
347
+ ): Map<string, UnansweredOwnerCall[]> =>
348
+ new Map([[toTmuxSessionName(issueUrl), calls]]);
349
+
350
+ it('builds version 5 with key order version, overviewUrl, tdpmConsoleUrl, newIssueUrl, groups', () => {
351
+ const result = run([makeIssue({ story: 'Story Alpha' })]);
352
+ expect(result.v5).not.toBeNull();
353
+ expect(Object.keys(result.v5 ?? {})).toEqual([
354
+ 'version',
355
+ 'overviewUrl',
356
+ 'tdpmConsoleUrl',
357
+ 'newIssueUrl',
358
+ 'groups',
359
+ ]);
360
+ expect(result.v5?.version).toBe(5);
361
+ });
362
+
363
+ it('gives a session with no unanswered call an empty unansweredCalls array', () => {
364
+ const result = run([makeIssue({ story: 'Story Alpha' })]);
365
+ expect(result.v5?.groups).toEqual([
366
+ {
367
+ story: 'Story Alpha',
368
+ sessions: [
369
+ {
370
+ name: 'https://github.com/demo/repo/issues/1',
371
+ description: 'Issue 1',
372
+ unansweredCalls: [],
373
+ },
374
+ ],
375
+ },
376
+ ]);
377
+ });
378
+
379
+ it('carries the unanswered calls of a session looked up by the tmux session name the reconciler derives from the issue url', () => {
380
+ const issue = makeIssue({ story: 'Story Alpha' });
381
+ const call: UnansweredOwnerCall = {
382
+ calledAt: '2026-08-13T10:14:00.000Z',
383
+ body: 'Please decide whether to merge the release branch',
384
+ };
385
+
386
+ const result = run([issue], {
387
+ unansweredCallsByTmuxSessionName: callsFor(issue.url, [call]),
388
+ });
389
+
390
+ expect(result.v5?.groups[0].sessions[0].unansweredCalls).toEqual([call]);
391
+ });
392
+
393
+ it('carries every unanswered call of one session in call order with its own time and body', () => {
394
+ const issue = makeIssue({ story: 'Story Alpha' });
395
+ const calls: UnansweredOwnerCall[] = [
396
+ { calledAt: '2026-08-13T10:14:00.000Z', body: 'first call body' },
397
+ { calledAt: '2026-08-13T10:41:30.000Z', body: 'second call body' },
398
+ ];
399
+
400
+ const result = run([issue], {
401
+ unansweredCallsByTmuxSessionName: callsFor(issue.url, calls),
402
+ });
403
+
404
+ expect(result.v5?.groups[0].sessions[0].unansweredCalls).toEqual(calls);
405
+ expect(
406
+ result.v5?.groups[0].sessions[0].unansweredCalls.map(
407
+ (unansweredCall) => unansweredCall.calledAt,
408
+ ),
409
+ ).toEqual(['2026-08-13T10:14:00.000Z', '2026-08-13T10:41:30.000Z']);
410
+ });
411
+
412
+ it('leaves the version 4 document free of unanswered call data', () => {
413
+ const issue = makeIssue({ story: 'Story Alpha' });
414
+
415
+ const result = run([issue], {
416
+ unansweredCallsByTmuxSessionName: callsFor(issue.url, [
417
+ { calledAt: '2026-08-13T10:14:00.000Z', body: 'first call body' },
418
+ ]),
419
+ });
420
+
421
+ expect(result.v4?.groups[0].sessions[0]).toEqual({
422
+ name: 'https://github.com/demo/repo/issues/1',
423
+ description: 'Issue 1',
424
+ });
425
+ });
426
+
427
+ it('is null when the console token is unset while v3 is still produced', () => {
428
+ const result = run([makeIssue({ story: 'Story Alpha' })], {
429
+ consoleToken: null,
430
+ });
431
+ expect(result.v5).toBeNull();
432
+ expect(result.v3).not.toBeNull();
433
+ });
434
+ });
336
435
  });
@@ -1,5 +1,7 @@
1
1
  import { Issue } from '../../entities/Issue';
2
2
  import { FieldOption, Project } from '../../entities/Project';
3
+ import { UnansweredOwnerCall } from '../../entities/UnansweredOwnerCall';
4
+ import { toTmuxSessionName } from './InTmuxByHumanSessionReconcileUseCase';
3
5
 
4
6
  export type InTmuxByHumanUrlEntry = {
5
7
  url: string;
@@ -26,6 +28,17 @@ export type InTmuxByHumanGroupV4 = {
26
28
  sessions: InTmuxByHumanSession[];
27
29
  };
28
30
 
31
+ export type InTmuxByHumanSessionV5 = {
32
+ name: string;
33
+ description: string;
34
+ unansweredCalls: UnansweredOwnerCall[];
35
+ };
36
+
37
+ export type InTmuxByHumanGroupV5 = {
38
+ story: string;
39
+ sessions: InTmuxByHumanSessionV5[];
40
+ };
41
+
29
42
  export type InTmuxByHumanV3 = {
30
43
  version: 3;
31
44
  overviewUrl: string;
@@ -41,11 +54,20 @@ export type InTmuxByHumanV4 = {
41
54
  groups: InTmuxByHumanGroupV4[];
42
55
  };
43
56
 
57
+ export type InTmuxByHumanV5 = {
58
+ version: 5;
59
+ overviewUrl: string;
60
+ tdpmConsoleUrl: string;
61
+ newIssueUrl: string;
62
+ groups: InTmuxByHumanGroupV5[];
63
+ };
64
+
44
65
  export type InTmuxByHumanData = {
45
66
  v1: InTmuxByHumanGroupV1[];
46
67
  v2: InTmuxByHumanGroupV2[];
47
68
  v3: InTmuxByHumanV3 | null;
48
69
  v4: InTmuxByHumanV4 | null;
70
+ v5: InTmuxByHumanV5 | null;
49
71
  };
50
72
 
51
73
  export type GenerateInTmuxByHumanDataInput = {
@@ -58,6 +80,7 @@ export type GenerateInTmuxByHumanDataInput = {
58
80
  newIssueRepo?: string;
59
81
  consoleBaseUrl: string | null;
60
82
  consoleToken: string | null;
83
+ unansweredCallsByTmuxSessionName: Map<string, UnansweredOwnerCall[]>;
61
84
  now: Date;
62
85
  };
63
86
 
@@ -81,6 +104,7 @@ export class GenerateInTmuxByHumanDataUseCase {
81
104
  newIssueRepo,
82
105
  consoleBaseUrl,
83
106
  consoleToken,
107
+ unansweredCallsByTmuxSessionName,
84
108
  } = input;
85
109
 
86
110
  const storyOrder = project.story
@@ -114,6 +138,17 @@ export class GenerateInTmuxByHumanDataUseCase {
114
138
  })),
115
139
  }));
116
140
 
141
+ const v5Groups: InTmuxByHumanGroupV5[] = groups.map((group) => ({
142
+ story: group.story,
143
+ sessions: group.issues.map((issue) => ({
144
+ name: issue.url,
145
+ description: issue.title,
146
+ unansweredCalls:
147
+ unansweredCallsByTmuxSessionName.get(toTmuxSessionName(issue.url)) ??
148
+ [],
149
+ })),
150
+ }));
151
+
117
152
  const overviewUrl = project.url;
118
153
  const tdpmConsoleUrl = consoleBaseUrl
119
154
  ? `${consoleBaseUrl}/projects/${pjcode}`
@@ -139,7 +174,18 @@ export class GenerateInTmuxByHumanDataUseCase {
139
174
  }
140
175
  : null;
141
176
 
142
- return { v1, v2, v3, v4 };
177
+ const v5: InTmuxByHumanV5 | null =
178
+ tdpmConsoleUrl && consoleToken
179
+ ? {
180
+ version: 5,
181
+ overviewUrl,
182
+ tdpmConsoleUrl: `${tdpmConsoleUrl}?k=${consoleToken}`,
183
+ newIssueUrl: `https://github.com/${org}/${newIssueRepo ?? repo}/issues/new?assignees=${assigneeLogin}`,
184
+ groups: v5Groups,
185
+ }
186
+ : null;
187
+
188
+ return { v1, v2, v3, v4, v5 };
143
189
  };
144
190
 
145
191
  private isInTmuxByHuman = (issue: Issue, assigneeLogin: string): boolean =>
@@ -0,0 +1,141 @@
1
+ import { Issue } from '../../entities/Issue';
2
+ import { IN_TMUX_STATUS_NAME } from '../../entities/WorkflowStatus';
3
+ import { FieldOption, Project } from '../../entities/Project';
4
+ import { UnansweredOwnerCall } from '../../entities/UnansweredOwnerCall';
5
+ import { IssueRepository } from '../adapter-interfaces/IssueRepository';
6
+ import { TmuxSessionRepository } from '../adapter-interfaces/TmuxSessionRepository';
7
+ import { InTmuxByHumanSessionReconcileUseCase } from './InTmuxByHumanSessionReconcileUseCase';
8
+ import { GenerateInTmuxByHumanDataUseCase } from './GenerateInTmuxByHumanDataUseCase';
9
+
10
+ const ASSIGNEE = 'owner-login';
11
+ const LAUNCHER = 'cl';
12
+ const NOW = new Date('2026-06-25T12:00:00.000Z');
13
+ const ISSUE_URL = 'https://github.com/demo/repo/issues/1';
14
+ const STORY_NAME = 'Story Alpha';
15
+
16
+ const fieldOption = (id: string, name: string): FieldOption => ({
17
+ id,
18
+ name,
19
+ color: 'BLUE',
20
+ description: '',
21
+ });
22
+
23
+ const projectWithStory: Project = {
24
+ id: 'project-node-id',
25
+ url: 'https://github.com/orgs/demo/projects/1',
26
+ databaseId: 1,
27
+ name: 'demo',
28
+ status: {
29
+ name: 'Status',
30
+ fieldId: 'status-field',
31
+ statuses: [fieldOption('st-tmux', IN_TMUX_STATUS_NAME)],
32
+ },
33
+ nextActionDate: null,
34
+ nextActionHour: null,
35
+ story: {
36
+ name: 'story',
37
+ fieldId: 'story-field',
38
+ databaseId: 2,
39
+ stories: [fieldOption('s1', STORY_NAME)],
40
+ workflowManagementStory: { id: 'wm', name: 'workflow management' },
41
+ },
42
+ remainingEstimationMinutes: null,
43
+ dependedIssueUrlSeparatedByComma: null,
44
+ completionDate50PercentConfidence: null,
45
+ };
46
+
47
+ const inTmuxIssue: Issue = {
48
+ nameWithOwner: 'demo/repo',
49
+ number: 1,
50
+ title: 'Issue 1',
51
+ state: 'OPEN',
52
+ status: IN_TMUX_STATUS_NAME,
53
+ story: STORY_NAME,
54
+ nextActionDate: null,
55
+ nextActionHour: null,
56
+ estimationMinutes: null,
57
+ dependedIssueUrls: [],
58
+ completionDate50PercentConfidence: null,
59
+ url: ISSUE_URL,
60
+ assignees: [ASSIGNEE],
61
+ labels: [],
62
+ org: 'demo',
63
+ repo: 'repo',
64
+ body: '',
65
+ itemId: 'item-1',
66
+ isPr: false,
67
+ isInProgress: false,
68
+ isClosed: false,
69
+ createdAt: NOW,
70
+ author: '',
71
+ closingIssueReferenceUrls: [],
72
+ };
73
+
74
+ const tmuxSessionNameTheReconcilerCreatesFor = async (
75
+ issue: Issue,
76
+ ): Promise<string> => {
77
+ const launchedSessionNames: string[] = [];
78
+ const tmuxSessionRepository: TmuxSessionRepository = {
79
+ listLiveSessionNames: async () => [],
80
+ listLiveSessionsWithActivity: async () => [],
81
+ listInteractiveProcessCommandLines: async () => [],
82
+ launchDetachedSession: async (sessionName: string) => {
83
+ launchedSessionNames.push(sessionName);
84
+ },
85
+ killSession: async () => undefined,
86
+ killOwnSession: async () => undefined,
87
+ sendKeys: async () => undefined,
88
+ launchBareNameLeaderSession: async () => undefined,
89
+ };
90
+ const issueStateRepository: Pick<
91
+ IssueRepository,
92
+ 'getIssueOrPullRequestState'
93
+ > = {
94
+ getIssueOrPullRequestState: async () => ({
95
+ state: 'OPEN',
96
+ merged: false,
97
+ isPullRequest: false,
98
+ title: 'Issue 1',
99
+ }),
100
+ };
101
+
102
+ await new InTmuxByHumanSessionReconcileUseCase(
103
+ tmuxSessionRepository,
104
+ issueStateRepository,
105
+ ).run({
106
+ issues: [issue],
107
+ assigneeLogin: ASSIGNEE,
108
+ launcherCommand: LAUNCHER,
109
+ now: NOW,
110
+ });
111
+
112
+ return launchedSessionNames[0];
113
+ };
114
+
115
+ describe('unanswered owner call session key contract', () => {
116
+ it('reads the calls under the very tmux session name the reconciler creates for that issue', async () => {
117
+ const call: UnansweredOwnerCall = {
118
+ calledAt: '2026-06-25T11:00:00.000Z',
119
+ body: 'Please decide whether to merge the release branch',
120
+ };
121
+ const sessionNameTheReconcilerCreates =
122
+ await tmuxSessionNameTheReconcilerCreatesFor(inTmuxIssue);
123
+
124
+ const result = new GenerateInTmuxByHumanDataUseCase().run({
125
+ project: projectWithStory,
126
+ issues: [inTmuxIssue],
127
+ pjcode: 'demo',
128
+ assigneeLogin: ASSIGNEE,
129
+ org: 'demo',
130
+ repo: 'repo',
131
+ consoleBaseUrl: 'https://console.example.test',
132
+ consoleToken: 'test-token-value',
133
+ unansweredCallsByTmuxSessionName: new Map([
134
+ [sessionNameTheReconcilerCreates, [call]],
135
+ ]),
136
+ now: NOW,
137
+ });
138
+
139
+ expect(result.v5?.groups[0].sessions[0].unansweredCalls).toEqual([call]);
140
+ });
141
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AA4CA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAqD3D,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,CAqwBP;CACH"}
1
+ {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAkDA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAqD3D,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,CA6yBP;CACH"}
@@ -1,5 +1,6 @@
1
1
  import type { Issue } from '../../../domain/entities/Issue';
2
2
  import type { Project } from '../../../domain/entities/Project';
3
+ import type { UnansweredOwnerCall } from '../../../domain/entities/UnansweredOwnerCall';
3
4
  export type InTmuxByHumanDataWriterParams = {
4
5
  inTmuxDataOutputDir: string | null | undefined;
5
6
  inTmuxConsoleBaseUrl: string | null | undefined;
@@ -12,6 +13,7 @@ export type InTmuxByHumanDataWriterParams = {
12
13
  newIssueRepo?: string | null | undefined;
13
14
  project: Project;
14
15
  issues: Issue[];
16
+ unansweredCallsByTmuxSessionName?: Map<string, UnansweredOwnerCall[]>;
15
17
  now: Date;
16
18
  };
17
19
  export declare const writeInTmuxByHumanData: (params: InTmuxByHumanDataWriterParams) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"inTmuxByHumanDataWriter.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAMhE,MAAM,MAAM,6BAA6B,GAAG;IAC1C,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,kBAAkB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,kBAAkB,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAUF,eAAO,MAAM,sBAAsB,GACjC,QAAQ,6BAA6B,KACpC,IA0EF,CAAC"}
1
+ {"version":3,"file":"inTmuxByHumanDataWriter.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAC;AAMxF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,kBAAkB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,kBAAkB,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,gCAAgC,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC;IACtE,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAUF,eAAO,MAAM,sBAAsB,GACjC,QAAQ,6BAA6B,KACpC,IA2FF,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { UnansweredOwnerCall } from '../../../domain/entities/UnansweredOwnerCall';
2
+ import { LiveSessionProcessSnapshotProvider } from '../../../domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider';
3
+ import { InteractiveLiveSessionTranscriptResolver } from '../../../domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver';
4
+ export type UnansweredOwnerCallListProvider = {
5
+ listUnansweredOwnerCallsBySessionName: (transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, UnansweredOwnerCall[]>>;
6
+ };
7
+ export type ResolveUnansweredOwnerCallsParams = {
8
+ liveSessionProcessSnapshotProvider: LiveSessionProcessSnapshotProvider;
9
+ interactiveLiveSessionTranscriptResolver: InteractiveLiveSessionTranscriptResolver;
10
+ unansweredOwnerCallListProvider: UnansweredOwnerCallListProvider;
11
+ };
12
+ export declare const resolveUnansweredOwnerCallsByTmuxSessionName: (params: ResolveUnansweredOwnerCallsParams) => Promise<Map<string, UnansweredOwnerCall[]>>;
13
+ //# sourceMappingURL=resolveUnansweredOwnerCalls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveUnansweredOwnerCalls.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/resolveUnansweredOwnerCalls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAC;AAGnF,OAAO,EAAE,kCAAkC,EAAE,MAAM,gFAAgF,CAAC;AACpI,OAAO,EAAE,wCAAwC,EAAE,MAAM,sFAAsF,CAAC;AAEhJ,MAAM,MAAM,+BAA+B,GAAG;IAC5C,qCAAqC,EAAE,CACrC,2BAA2B,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,kCAAkC,EAAE,kCAAkC,CAAC;IACvE,wCAAwC,EAAE,wCAAwC,CAAC;IACnF,+BAA+B,EAAE,+BAA+B,CAAC;CAClE,CAAC;AAEF,eAAO,MAAM,4CAA4C,GACvD,QAAQ,iCAAiC,KACxC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAY5C,CAAC"}
@@ -1,12 +1,17 @@
1
1
  import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
2
+ import { UnansweredOwnerCall } from '../../domain/entities/UnansweredOwnerCall';
2
3
  export declare const ownerCallMarkerFamilyResolve: (marker: string) => string[];
3
4
  export declare class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
4
5
  private readonly ownerReplyMarkerDirectory;
5
6
  private readonly ownerCallMarkerFamily;
6
7
  constructor(ownerCallMarker: string | null, ownerReplyMarkerDirectory?: string | null);
7
8
  listUnansweredOwnerCallEpochSecondsBySessionName: (transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, number>>;
9
+ listUnansweredOwnerCallsBySessionName: (transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, UnansweredOwnerCall[]>>;
10
+ private scanTranscript;
11
+ private resolveReplyEpochMs;
8
12
  private findUnansweredOwnerCallEpochMs;
9
13
  private readOwnerReplyMarkerEpochMs;
14
+ private isCallDeliveredToOwner;
10
15
  private isCallSuppressedUndelivered;
11
16
  private isCandidateCallDelivered;
12
17
  private readMarkerEpochMs;
@@ -1 +1 @@
1
- {"version":3,"file":"TranscriptOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AA6F3G,eAAO,MAAM,4BAA4B,GAAI,QAAQ,MAAM,KAAG,MAAM,EAMtD,CAAC;AA4Cf,qBAAa,iCAAkC,YAAW,uBAAuB;IAK7E,OAAO,CAAC,QAAQ,CAAC,yBAAyB;IAJ5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAW;gBAG/C,eAAe,EAAE,MAAM,GAAG,IAAI,EACb,yBAAyB,GAAE,MAAM,GAAG,IAAW;IAQlE,gDAAgD,GAC9C,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqB7B;IAEF,OAAO,CAAC,8BAA8B,CAqFpC;IAOF,OAAO,CAAC,2BAA2B,CAGyC;IAS5E,OAAO,CAAC,2BAA2B,CAgBjC;IAEF,OAAO,CAAC,wBAAwB,CAoB9B;IAEF,OAAO,CAAC,iBAAiB,CA4BvB;CACH"}
1
+ {"version":3,"file":"TranscriptOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AAC3G,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAwGhF,eAAO,MAAM,4BAA4B,GAAI,QAAQ,MAAM,KAAG,MAAM,EAMtD,CAAC;AA4Cf,qBAAa,iCAAkC,YAAW,uBAAuB;IAK7E,OAAO,CAAC,QAAQ,CAAC,yBAAyB;IAJ5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAW;gBAG/C,eAAe,EAAE,MAAM,GAAG,IAAI,EACb,yBAAyB,GAAE,MAAM,GAAG,IAAW;IAQlE,gDAAgD,GAC9C,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqB7B;IAEF,qCAAqC,GACnC,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC,CAqC5C;IAEF,OAAO,CAAC,cAAc,CA6DpB;IAEF,OAAO,CAAC,mBAAmB,CAUzB;IAEF,OAAO,CAAC,8BAA8B,CAiCpC;IAOF,OAAO,CAAC,2BAA2B,CAGyC;IAS5E,OAAO,CAAC,sBAAsB,CAW5B;IAEF,OAAO,CAAC,2BAA2B,CAgBjC;IAEF,OAAO,CAAC,wBAAwB,CAoB9B;IAEF,OAAO,CAAC,iBAAiB,CA4BvB;CACH"}
@@ -0,0 +1,5 @@
1
+ export type UnansweredOwnerCall = {
2
+ calledAt: string;
3
+ body: string;
4
+ };
5
+ //# sourceMappingURL=UnansweredOwnerCall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnansweredOwnerCall.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/UnansweredOwnerCall.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC"}