github-issue-tower-defence-management 1.109.1 → 1.110.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 (224) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +29 -3
  3. package/bin/adapter/entry-points/cli/index.js +10 -10
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/dashboardComposeService.js +7 -6
  6. package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  10. package/bin/adapter/entry-points/console/webServer.js +5 -4
  11. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  12. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +125 -2
  13. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  14. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js +2 -0
  15. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js.map +1 -1
  16. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +2 -0
  17. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
  18. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +2 -0
  19. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
  20. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +62 -0
  21. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -0
  22. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +88 -3
  23. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  24. package/bin/adapter/proxy/RateLimitCache.js +25 -1
  25. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  26. package/bin/adapter/proxy/proxyEntry.js +25 -0
  27. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  28. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +37 -0
  29. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -0
  30. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +76 -0
  31. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -0
  32. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js +67 -0
  33. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js.map +1 -0
  34. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js +50 -0
  35. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js.map +1 -0
  36. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +12 -0
  37. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -0
  38. package/bin/adapter/repositories/NodeSubAgentProcessLister.js +37 -0
  39. package/bin/adapter/repositories/NodeSubAgentProcessLister.js.map +1 -0
  40. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +48 -0
  41. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -0
  42. package/bin/adapter/repositories/RealSleeper.js +17 -0
  43. package/bin/adapter/repositories/RealSleeper.js.map +1 -0
  44. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +56 -0
  45. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -0
  46. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +150 -0
  47. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -0
  48. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +152 -0
  49. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -0
  50. package/bin/domain/entities/LiveSessionActivitySnapshot.js +3 -0
  51. package/bin/domain/entities/LiveSessionActivitySnapshot.js.map +1 -0
  52. package/bin/domain/entities/LiveSessionOutputActivity.js +3 -0
  53. package/bin/domain/entities/LiveSessionOutputActivity.js.map +1 -0
  54. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +35 -0
  55. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -0
  56. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +113 -0
  57. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -0
  58. package/bin/domain/usecases/OauthTokenSelectUseCase.js +8 -2
  59. package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
  60. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js +3 -0
  61. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js.map +1 -0
  62. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js +3 -0
  63. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js.map +1 -0
  64. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js +3 -0
  65. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js.map +1 -0
  66. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js +3 -0
  67. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js.map +1 -0
  68. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js +3 -0
  69. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js.map +1 -0
  70. package/bin/domain/usecases/adapter-interfaces/Sleeper.js +3 -0
  71. package/bin/domain/usecases/adapter-interfaces/Sleeper.js.map +1 -0
  72. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js +3 -0
  73. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js.map +1 -0
  74. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js +3 -0
  75. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js.map +1 -0
  76. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js +3 -0
  77. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js.map +1 -0
  78. package/bin/domain/usecases/dashboard/DashboardProjectCode.js +19 -0
  79. package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -0
  80. package/package.json +1 -1
  81. package/src/adapter/entry-points/cli/index.ts +14 -14
  82. package/src/adapter/entry-points/console/dashboardComposeService.test.ts +31 -28
  83. package/src/adapter/entry-points/console/dashboardComposeService.ts +13 -10
  84. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +175 -0
  85. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -1
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.stories.tsx +24 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.test.tsx +63 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.tsx +51 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +7 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +2 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +18 -0
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +2 -0
  93. package/src/adapter/entry-points/console/ui/src/index.css +14 -0
  94. package/src/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  95. package/src/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  96. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  97. package/src/adapter/entry-points/console/webServer.test.ts +23 -23
  98. package/src/adapter/entry-points/console/webServer.ts +5 -4
  99. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +122 -0
  100. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.ts +2 -0
  101. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +112 -0
  102. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +2 -0
  103. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.test.ts +131 -0
  104. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +2 -0
  105. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +338 -0
  106. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +174 -0
  107. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +79 -0
  108. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +117 -3
  109. package/src/adapter/proxy/RateLimitCache.test.ts +89 -0
  110. package/src/adapter/proxy/RateLimitCache.ts +31 -0
  111. package/src/adapter/proxy/proxyEntry.test.ts +92 -0
  112. package/src/adapter/proxy/proxyEntry.ts +32 -0
  113. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +77 -0
  114. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +51 -0
  115. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +92 -0
  116. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +48 -0
  117. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.test.ts +56 -0
  118. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.ts +32 -0
  119. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.test.ts +32 -0
  120. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.ts +17 -0
  121. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +12 -0
  122. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +9 -0
  123. package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +43 -0
  124. package/src/adapter/repositories/NodeSubAgentProcessLister.ts +40 -0
  125. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +109 -0
  126. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +56 -0
  127. package/src/adapter/repositories/RealSleeper.test.ts +19 -0
  128. package/src/adapter/repositories/RealSleeper.ts +12 -0
  129. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +153 -0
  130. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +83 -0
  131. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +220 -0
  132. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +139 -0
  133. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +217 -0
  134. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +145 -0
  135. package/src/domain/entities/LiveSessionActivitySnapshot.ts +12 -0
  136. package/src/domain/entities/LiveSessionOutputActivity.ts +4 -0
  137. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +52 -0
  138. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +39 -0
  139. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +2 -0
  140. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +40 -0
  141. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +612 -0
  142. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +223 -0
  143. package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +36 -0
  144. package/src/domain/usecases/OauthTokenSelectUseCase.ts +12 -0
  145. package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +5 -0
  146. package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +7 -0
  147. package/src/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.ts +7 -0
  148. package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +6 -0
  149. package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +11 -0
  150. package/src/domain/usecases/adapter-interfaces/Sleeper.ts +3 -0
  151. package/src/domain/usecases/adapter-interfaces/SubAgentProcessLister.ts +8 -0
  152. package/src/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.ts +3 -0
  153. package/src/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.ts +3 -0
  154. package/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +32 -0
  155. package/src/domain/usecases/dashboard/DashboardProjectCode.ts +20 -0
  156. package/types/adapter/entry-points/console/dashboardComposeService.d.ts +1 -1
  157. package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -1
  158. package/types/adapter/entry-points/console/webServer.d.ts +2 -2
  159. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  160. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  161. package/types/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.d.ts.map +1 -1
  162. package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
  163. package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
  164. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +34 -0
  165. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -0
  166. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +1 -0
  167. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  168. package/types/adapter/proxy/RateLimitCache.d.ts +3 -0
  169. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  170. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  171. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +15 -0
  172. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -0
  173. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +10 -0
  174. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -0
  175. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts +9 -0
  176. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts.map +1 -0
  177. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts +7 -0
  178. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  179. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +5 -0
  180. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -0
  181. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts +8 -0
  182. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts.map +1 -0
  183. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts +12 -0
  184. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -0
  185. package/types/adapter/repositories/RealSleeper.d.ts +5 -0
  186. package/types/adapter/repositories/RealSleeper.d.ts.map +1 -0
  187. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +13 -0
  188. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -0
  189. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +10 -0
  190. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -0
  191. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +12 -0
  192. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -0
  193. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +12 -0
  194. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -0
  195. package/types/domain/entities/LiveSessionOutputActivity.d.ts +5 -0
  196. package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -0
  197. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +7 -0
  198. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -0
  199. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +41 -0
  200. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -0
  201. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +2 -0
  202. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
  203. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +4 -0
  204. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -0
  205. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +5 -0
  206. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -0
  207. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts +5 -0
  208. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts.map +1 -0
  209. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +6 -0
  210. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -0
  211. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +6 -0
  212. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -0
  213. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts +4 -0
  214. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts.map +1 -0
  215. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts +8 -0
  216. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts.map +1 -0
  217. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts +4 -0
  218. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts.map +1 -0
  219. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts +4 -0
  220. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  221. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +4 -0
  222. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts.map +1 -0
  223. package/bin/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
  224. package/src/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
@@ -0,0 +1,153 @@
1
+ import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
+ import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
3
+ import { TmuxSilentSessionNotificationRepository } from './TmuxSilentSessionNotificationRepository';
4
+
5
+ type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
6
+
7
+ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
8
+ runCommand: jest.fn().mockResolvedValue({
9
+ stdout: '',
10
+ stderr: '',
11
+ exitCode: 0,
12
+ }),
13
+ });
14
+
15
+ const createMockCacheRepository = (): Mocked<
16
+ Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>
17
+ > => ({
18
+ getLatest: jest.fn().mockResolvedValue(null),
19
+ set: jest.fn().mockResolvedValue(undefined),
20
+ });
21
+
22
+ describe('TmuxSilentSessionNotificationRepository', () => {
23
+ describe('sendSelfCheckNotification', () => {
24
+ it('sends the message literally then submits it with Enter', async () => {
25
+ const runner = createMockRunner();
26
+ const cache = createMockCacheRepository();
27
+ const repository = new TmuxSilentSessionNotificationRepository(
28
+ runner,
29
+ cache,
30
+ );
31
+
32
+ await repository.sendSelfCheckNotification(
33
+ 'https_//github_com/owner/repo/issues/9',
34
+ 'self check message',
35
+ );
36
+
37
+ expect(runner.runCommand.mock.calls[0]).toEqual([
38
+ 'tmux',
39
+ [
40
+ 'send-keys',
41
+ '-t',
42
+ 'https_//github_com/owner/repo/issues/9',
43
+ '-l',
44
+ 'self check message',
45
+ ],
46
+ ]);
47
+ expect(runner.runCommand.mock.calls[1]).toEqual([
48
+ 'tmux',
49
+ ['send-keys', '-t', 'https_//github_com/owner/repo/issues/9', 'Enter'],
50
+ ]);
51
+ });
52
+
53
+ it('throws when sending the message literally fails', async () => {
54
+ const runner = createMockRunner();
55
+ runner.runCommand.mockResolvedValueOnce({
56
+ stdout: '',
57
+ stderr: "can't find session",
58
+ exitCode: 1,
59
+ });
60
+ const cache = createMockCacheRepository();
61
+ const repository = new TmuxSilentSessionNotificationRepository(
62
+ runner,
63
+ cache,
64
+ );
65
+
66
+ await expect(
67
+ repository.sendSelfCheckNotification('missing_session', 'message'),
68
+ ).rejects.toThrow(
69
+ 'Failed to send notification to tmux session "missing_session"',
70
+ );
71
+ });
72
+ });
73
+
74
+ describe('getLastNotifiedEpochSeconds', () => {
75
+ it('returns the cached epoch seconds for a session', async () => {
76
+ const runner = createMockRunner();
77
+ const cache = createMockCacheRepository();
78
+ cache.getLatest.mockResolvedValue({
79
+ value: { epochSeconds: 1700000000 },
80
+ timestamp: new Date(),
81
+ });
82
+ const repository = new TmuxSilentSessionNotificationRepository(
83
+ runner,
84
+ cache,
85
+ );
86
+
87
+ const result = await repository.getLastNotifiedEpochSeconds(
88
+ 'https_//github_com/owner/repo/issues/9',
89
+ );
90
+
91
+ expect(result).toBe(1700000000);
92
+ expect(cache.getLatest).toHaveBeenCalledWith(
93
+ 'silent-session-notification/https___github_com_owner_repo_issues_9',
94
+ );
95
+ });
96
+
97
+ it('returns null when no cache entry exists', async () => {
98
+ const runner = createMockRunner();
99
+ const cache = createMockCacheRepository();
100
+ cache.getLatest.mockResolvedValue(null);
101
+ const repository = new TmuxSilentSessionNotificationRepository(
102
+ runner,
103
+ cache,
104
+ );
105
+
106
+ const result = await repository.getLastNotifiedEpochSeconds(
107
+ 'https_//github_com/owner/repo/issues/9',
108
+ );
109
+
110
+ expect(result).toBeNull();
111
+ });
112
+
113
+ it('returns null when the cached value has no numeric epoch', async () => {
114
+ const runner = createMockRunner();
115
+ const cache = createMockCacheRepository();
116
+ cache.getLatest.mockResolvedValue({
117
+ value: {},
118
+ timestamp: new Date(),
119
+ });
120
+ const repository = new TmuxSilentSessionNotificationRepository(
121
+ runner,
122
+ cache,
123
+ );
124
+
125
+ const result = await repository.getLastNotifiedEpochSeconds(
126
+ 'https_//github_com/owner/repo/issues/9',
127
+ );
128
+
129
+ expect(result).toBeNull();
130
+ });
131
+ });
132
+
133
+ describe('setLastNotifiedEpochSeconds', () => {
134
+ it('persists the epoch seconds keyed by the sanitized session name', async () => {
135
+ const runner = createMockRunner();
136
+ const cache = createMockCacheRepository();
137
+ const repository = new TmuxSilentSessionNotificationRepository(
138
+ runner,
139
+ cache,
140
+ );
141
+
142
+ await repository.setLastNotifiedEpochSeconds(
143
+ 'https_//github_com/owner/repo/issues/9',
144
+ 1700000000,
145
+ );
146
+
147
+ expect(cache.set).toHaveBeenCalledWith(
148
+ 'silent-session-notification/https___github_com_owner_repo_issues_9',
149
+ { epochSeconds: 1700000000 },
150
+ );
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,83 @@
1
+ import { SilentSessionNotificationRepository } from '../../domain/usecases/adapter-interfaces/SilentSessionNotificationRepository';
2
+ import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
3
+ import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
4
+
5
+ const CACHE_KEY_PREFIX = 'silent-session-notification';
6
+
7
+ const readEpochSeconds = (value: object): number | null => {
8
+ if (!('epochSeconds' in value)) {
9
+ return null;
10
+ }
11
+ const candidate = value.epochSeconds;
12
+ if (typeof candidate !== 'number') {
13
+ return null;
14
+ }
15
+ return candidate;
16
+ };
17
+
18
+ export class TmuxSilentSessionNotificationRepository implements SilentSessionNotificationRepository {
19
+ constructor(
20
+ private readonly localCommandRunner: LocalCommandRunner,
21
+ private readonly cacheRepository: Pick<
22
+ LocalStorageCacheRepository,
23
+ 'getLatest' | 'set'
24
+ >,
25
+ ) {}
26
+
27
+ getLastNotifiedEpochSeconds = async (
28
+ sessionName: string,
29
+ ): Promise<number | null> => {
30
+ const cached = await this.cacheRepository.getLatest(
31
+ this.toCacheKey(sessionName),
32
+ );
33
+ if (cached === null) {
34
+ return null;
35
+ }
36
+ return readEpochSeconds(cached.value);
37
+ };
38
+
39
+ setLastNotifiedEpochSeconds = async (
40
+ sessionName: string,
41
+ epochSeconds: number,
42
+ ): Promise<void> => {
43
+ await this.cacheRepository.set(this.toCacheKey(sessionName), {
44
+ epochSeconds,
45
+ });
46
+ };
47
+
48
+ sendSelfCheckNotification = async (
49
+ sessionName: string,
50
+ message: string,
51
+ ): Promise<void> => {
52
+ const literalResult = await this.localCommandRunner.runCommand('tmux', [
53
+ 'send-keys',
54
+ '-t',
55
+ sessionName,
56
+ '-l',
57
+ message,
58
+ ]);
59
+ if (literalResult.exitCode !== 0) {
60
+ throw new Error(
61
+ `Failed to send notification to tmux session "${sessionName}": exit code ${literalResult.exitCode}${
62
+ literalResult.stderr ? `: ${literalResult.stderr}` : ''
63
+ }`,
64
+ );
65
+ }
66
+ const submitResult = await this.localCommandRunner.runCommand('tmux', [
67
+ 'send-keys',
68
+ '-t',
69
+ sessionName,
70
+ 'Enter',
71
+ ]);
72
+ if (submitResult.exitCode !== 0) {
73
+ throw new Error(
74
+ `Failed to send notification to tmux session "${sessionName}": exit code ${submitResult.exitCode}${
75
+ submitResult.stderr ? `: ${submitResult.stderr}` : ''
76
+ }`,
77
+ );
78
+ }
79
+ };
80
+
81
+ private toCacheKey = (sessionName: string): string =>
82
+ `${CACHE_KEY_PREFIX}/${sessionName.replace(/\//g, '_')}`;
83
+ }
@@ -0,0 +1,220 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { TranscriptOwnerCallStatusProvider } from './TranscriptOwnerCallStatusProvider';
5
+
6
+ describe('TranscriptOwnerCallStatusProvider', () => {
7
+ let rootDirectory: string;
8
+
9
+ beforeEach(() => {
10
+ rootDirectory = fs.mkdtempSync(
11
+ path.join(os.tmpdir(), 'owner-call-status-'),
12
+ );
13
+ });
14
+
15
+ afterEach(() => {
16
+ fs.rmSync(rootDirectory, { force: true, recursive: true });
17
+ });
18
+
19
+ const writeTranscript = (sessionName: string, lines: object[]): void => {
20
+ const fileName = `${sessionName.replace(/\//g, '_')}.jsonl`;
21
+ const filePath = path.join(rootDirectory, fileName);
22
+ fs.writeFileSync(
23
+ filePath,
24
+ lines.map((line) => JSON.stringify(line)).join('\n'),
25
+ 'utf8',
26
+ );
27
+ };
28
+
29
+ const assistantWithMarker = (timestamp: string): object => ({
30
+ type: 'assistant',
31
+ timestamp,
32
+ message: {
33
+ role: 'assistant',
34
+ stop_reason: 'end_turn',
35
+ content: [
36
+ { type: 'text', text: 'Waiting <<OWNER_CALL>> please decide.' },
37
+ ],
38
+ },
39
+ });
40
+
41
+ const assistantPlain = (timestamp: string): object => ({
42
+ type: 'assistant',
43
+ timestamp,
44
+ message: {
45
+ role: 'assistant',
46
+ stop_reason: 'end_turn',
47
+ content: [{ type: 'text', text: 'progress update' }],
48
+ },
49
+ });
50
+
51
+ const ownerReply = (timestamp: string): object => ({
52
+ type: 'user',
53
+ timestamp,
54
+ message: { role: 'user', content: 'go ahead' },
55
+ });
56
+
57
+ const toolResult = (timestamp: string): object => ({
58
+ type: 'user',
59
+ timestamp,
60
+ message: {
61
+ role: 'user',
62
+ content: [{ type: 'tool_result', content: 'ok' }],
63
+ },
64
+ });
65
+
66
+ it('reports a session as waiting when the last owner call is newer than the last owner reply', async () => {
67
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
68
+ writeTranscript(sessionName, [
69
+ ownerReply('2026-06-27T10:00:00.000Z'),
70
+ assistantWithMarker('2026-06-27T10:05:00.000Z'),
71
+ ]);
72
+ const provider = new TranscriptOwnerCallStatusProvider(
73
+ rootDirectory,
74
+ '<<OWNER_CALL>>',
75
+ );
76
+
77
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
78
+ sessionName,
79
+ ]);
80
+
81
+ expect(result.has(sessionName)).toBe(true);
82
+ });
83
+
84
+ it('does not report a session as waiting when a later owner reply followed the call', async () => {
85
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
86
+ writeTranscript(sessionName, [
87
+ assistantWithMarker('2026-06-27T10:00:00.000Z'),
88
+ ownerReply('2026-06-27T10:05:00.000Z'),
89
+ ]);
90
+ const provider = new TranscriptOwnerCallStatusProvider(
91
+ rootDirectory,
92
+ '<<OWNER_CALL>>',
93
+ );
94
+
95
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
96
+ sessionName,
97
+ ]);
98
+
99
+ expect(result.has(sessionName)).toBe(false);
100
+ });
101
+
102
+ it('compares full datetimes rather than only the time of day', async () => {
103
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
104
+ writeTranscript(sessionName, [
105
+ assistantWithMarker('2026-06-27T23:00:00.000Z'),
106
+ ownerReply('2026-06-28T01:00:00.000Z'),
107
+ ]);
108
+ const provider = new TranscriptOwnerCallStatusProvider(
109
+ rootDirectory,
110
+ '<<OWNER_CALL>>',
111
+ );
112
+
113
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
114
+ sessionName,
115
+ ]);
116
+
117
+ expect(result.has(sessionName)).toBe(false);
118
+ });
119
+
120
+ it('treats tool results as not being owner replies', async () => {
121
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
122
+ writeTranscript(sessionName, [
123
+ assistantWithMarker('2026-06-27T10:00:00.000Z'),
124
+ toolResult('2026-06-27T10:05:00.000Z'),
125
+ ]);
126
+ const provider = new TranscriptOwnerCallStatusProvider(
127
+ rootDirectory,
128
+ '<<OWNER_CALL>>',
129
+ );
130
+
131
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
132
+ sessionName,
133
+ ]);
134
+
135
+ expect(result.has(sessionName)).toBe(true);
136
+ });
137
+
138
+ it('does not report a session that never raised an owner call', async () => {
139
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
140
+ writeTranscript(sessionName, [
141
+ assistantPlain('2026-06-27T10:00:00.000Z'),
142
+ ownerReply('2026-06-27T10:05:00.000Z'),
143
+ ]);
144
+ const provider = new TranscriptOwnerCallStatusProvider(
145
+ rootDirectory,
146
+ '<<OWNER_CALL>>',
147
+ );
148
+
149
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
150
+ sessionName,
151
+ ]);
152
+
153
+ expect(result.has(sessionName)).toBe(false);
154
+ });
155
+
156
+ it('returns an empty set when the root directory is null', async () => {
157
+ const provider = new TranscriptOwnerCallStatusProvider(
158
+ null,
159
+ '<<OWNER_CALL>>',
160
+ );
161
+
162
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
163
+ 'https_//github_com/owner/repo/issues/9',
164
+ ]);
165
+
166
+ expect(result.size).toBe(0);
167
+ });
168
+
169
+ it('returns an empty set when the marker is null', async () => {
170
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
171
+ writeTranscript(sessionName, [
172
+ ownerReply('2026-06-27T10:00:00.000Z'),
173
+ assistantWithMarker('2026-06-27T10:05:00.000Z'),
174
+ ]);
175
+ const provider = new TranscriptOwnerCallStatusProvider(rootDirectory, null);
176
+
177
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
178
+ sessionName,
179
+ ]);
180
+
181
+ expect(result.size).toBe(0);
182
+ });
183
+
184
+ it('ignores a session whose transcript file is missing', async () => {
185
+ const provider = new TranscriptOwnerCallStatusProvider(
186
+ rootDirectory,
187
+ '<<OWNER_CALL>>',
188
+ );
189
+
190
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
191
+ 'https_//github_com/owner/repo/issues/9',
192
+ ]);
193
+
194
+ expect(result.size).toBe(0);
195
+ });
196
+
197
+ it('ignores malformed transcript lines', async () => {
198
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
199
+ const fileName = `${sessionName.replace(/\//g, '_')}.jsonl`;
200
+ const filePath = path.join(rootDirectory, fileName);
201
+ fs.writeFileSync(
202
+ filePath,
203
+ [
204
+ 'not json',
205
+ JSON.stringify(assistantWithMarker('2026-06-27T10:05:00.000Z')),
206
+ ].join('\n'),
207
+ 'utf8',
208
+ );
209
+ const provider = new TranscriptOwnerCallStatusProvider(
210
+ rootDirectory,
211
+ '<<OWNER_CALL>>',
212
+ );
213
+
214
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
215
+ sessionName,
216
+ ]);
217
+
218
+ expect(result.has(sessionName)).toBe(true);
219
+ });
220
+ });
@@ -0,0 +1,139 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
4
+
5
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
6
+ typeof value === 'object' && value !== null;
7
+
8
+ const readString = (
9
+ value: Record<string, unknown>,
10
+ key: string,
11
+ ): string | null => {
12
+ const candidate = value[key];
13
+ return typeof candidate === 'string' ? candidate : null;
14
+ };
15
+
16
+ const parseEpochMilliseconds = (timestamp: string | null): number | null => {
17
+ if (timestamp === null) {
18
+ return null;
19
+ }
20
+ const parsed = Date.parse(timestamp);
21
+ return Number.isNaN(parsed) ? null : parsed;
22
+ };
23
+
24
+ const extractText = (content: unknown): string => {
25
+ if (typeof content === 'string') {
26
+ return content;
27
+ }
28
+ if (!Array.isArray(content)) {
29
+ return '';
30
+ }
31
+ const texts: string[] = [];
32
+ for (const block of content) {
33
+ if (isRecord(block) && block.type === 'text') {
34
+ const text = readString(block, 'text');
35
+ if (text !== null) {
36
+ texts.push(text);
37
+ }
38
+ }
39
+ }
40
+ return texts.join('\n');
41
+ };
42
+
43
+ const hasOwnerTextReply = (content: unknown): boolean => {
44
+ if (typeof content === 'string') {
45
+ return content.length > 0;
46
+ }
47
+ if (!Array.isArray(content)) {
48
+ return false;
49
+ }
50
+ return content.some((block) => isRecord(block) && block.type === 'text');
51
+ };
52
+
53
+ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
54
+ constructor(
55
+ private readonly rootDirectory: string | null,
56
+ private readonly ownerCallMarker: string | null,
57
+ ) {}
58
+
59
+ listSessionNamesWithUnansweredOwnerCall = async (
60
+ sessionNames: string[],
61
+ ): Promise<Set<string>> => {
62
+ const waiting = new Set<string>();
63
+ if (
64
+ this.rootDirectory === null ||
65
+ this.ownerCallMarker === null ||
66
+ this.ownerCallMarker.length === 0
67
+ ) {
68
+ return waiting;
69
+ }
70
+ for (const sessionName of sessionNames) {
71
+ if (this.isWaitingForOwnerReply(sessionName, this.ownerCallMarker)) {
72
+ waiting.add(sessionName);
73
+ }
74
+ }
75
+ return waiting;
76
+ };
77
+
78
+ private isWaitingForOwnerReply = (
79
+ sessionName: string,
80
+ marker: string,
81
+ ): boolean => {
82
+ if (this.rootDirectory === null) {
83
+ return false;
84
+ }
85
+ const filePath = path.join(
86
+ this.rootDirectory,
87
+ this.toTranscriptFileName(sessionName),
88
+ );
89
+ let content: string;
90
+ try {
91
+ content = fs.readFileSync(filePath, 'utf8');
92
+ } catch {
93
+ return false;
94
+ }
95
+ let lastOwnerCallEpochMs: number | null = null;
96
+ let lastOwnerReplyEpochMs: number | null = null;
97
+ for (const line of content.split('\n')) {
98
+ const trimmed = line.trim();
99
+ if (trimmed.length === 0) {
100
+ continue;
101
+ }
102
+ let parsed: unknown;
103
+ try {
104
+ parsed = JSON.parse(trimmed);
105
+ } catch {
106
+ continue;
107
+ }
108
+ if (!isRecord(parsed)) {
109
+ continue;
110
+ }
111
+ const epochMs = parseEpochMilliseconds(readString(parsed, 'timestamp'));
112
+ if (epochMs === null) {
113
+ continue;
114
+ }
115
+ const type = readString(parsed, 'type');
116
+ const message = parsed.message;
117
+ const messageContent = isRecord(message) ? message.content : null;
118
+ if (
119
+ type === 'assistant' &&
120
+ extractText(messageContent).includes(marker)
121
+ ) {
122
+ lastOwnerCallEpochMs = epochMs;
123
+ }
124
+ if (type === 'user' && hasOwnerTextReply(messageContent)) {
125
+ lastOwnerReplyEpochMs = epochMs;
126
+ }
127
+ }
128
+ if (lastOwnerCallEpochMs === null) {
129
+ return false;
130
+ }
131
+ return (
132
+ lastOwnerReplyEpochMs === null ||
133
+ lastOwnerCallEpochMs > lastOwnerReplyEpochMs
134
+ );
135
+ };
136
+
137
+ private toTranscriptFileName = (sessionName: string): string =>
138
+ `${sessionName.replace(/\//g, '_')}.jsonl`;
139
+ }