github-issue-tower-defence-management 1.109.2 → 1.110.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +26 -0
  3. package/bin/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  4. package/bin/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  5. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +125 -2
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  8. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js +2 -0
  9. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js.map +1 -1
  10. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +2 -0
  11. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
  12. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +2 -0
  13. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
  14. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +62 -0
  15. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -0
  16. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +88 -3
  17. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  18. package/bin/adapter/proxy/RateLimitCache.js +33 -1
  19. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  20. package/bin/adapter/proxy/proxyEntry.js +25 -0
  21. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  22. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +37 -0
  23. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -0
  24. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +76 -0
  25. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -0
  26. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js +67 -0
  27. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js.map +1 -0
  28. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js +50 -0
  29. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js.map +1 -0
  30. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +12 -0
  31. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -0
  32. package/bin/adapter/repositories/NodeSubAgentProcessLister.js +37 -0
  33. package/bin/adapter/repositories/NodeSubAgentProcessLister.js.map +1 -0
  34. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +48 -0
  35. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -0
  36. package/bin/adapter/repositories/RealSleeper.js +17 -0
  37. package/bin/adapter/repositories/RealSleeper.js.map +1 -0
  38. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +56 -0
  39. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -0
  40. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +150 -0
  41. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -0
  42. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +152 -0
  43. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -0
  44. package/bin/domain/entities/LiveSessionActivitySnapshot.js +3 -0
  45. package/bin/domain/entities/LiveSessionActivitySnapshot.js.map +1 -0
  46. package/bin/domain/entities/LiveSessionOutputActivity.js +3 -0
  47. package/bin/domain/entities/LiveSessionOutputActivity.js.map +1 -0
  48. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +35 -0
  49. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -0
  50. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +113 -0
  51. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -0
  52. package/bin/domain/usecases/OauthTokenSelectUseCase.js +8 -2
  53. package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
  54. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js +3 -0
  55. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js.map +1 -0
  56. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js +3 -0
  57. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js.map +1 -0
  58. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js +3 -0
  59. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js.map +1 -0
  60. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js +3 -0
  61. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js.map +1 -0
  62. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js +3 -0
  63. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js.map +1 -0
  64. package/bin/domain/usecases/adapter-interfaces/Sleeper.js +3 -0
  65. package/bin/domain/usecases/adapter-interfaces/Sleeper.js.map +1 -0
  66. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js +3 -0
  67. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js.map +1 -0
  68. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js +3 -0
  69. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js.map +1 -0
  70. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js +3 -0
  71. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js.map +1 -0
  72. package/package.json +1 -1
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.stories.tsx +24 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.test.tsx +63 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.tsx +51 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +7 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +2 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +18 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +2 -0
  80. package/src/adapter/entry-points/console/ui/src/index.css +14 -0
  81. package/src/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  82. package/src/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  83. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  84. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +122 -0
  85. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.ts +2 -0
  86. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +112 -0
  87. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +2 -0
  88. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.test.ts +131 -0
  89. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +2 -0
  90. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +338 -0
  91. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +174 -0
  92. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +79 -0
  93. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +117 -3
  94. package/src/adapter/proxy/RateLimitCache.test.ts +152 -0
  95. package/src/adapter/proxy/RateLimitCache.ts +42 -0
  96. package/src/adapter/proxy/proxyEntry.test.ts +92 -0
  97. package/src/adapter/proxy/proxyEntry.ts +32 -0
  98. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +77 -0
  99. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +51 -0
  100. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +92 -0
  101. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +48 -0
  102. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.test.ts +56 -0
  103. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.ts +32 -0
  104. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.test.ts +32 -0
  105. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.ts +17 -0
  106. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +12 -0
  107. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +9 -0
  108. package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +43 -0
  109. package/src/adapter/repositories/NodeSubAgentProcessLister.ts +40 -0
  110. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +109 -0
  111. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +56 -0
  112. package/src/adapter/repositories/RealSleeper.test.ts +19 -0
  113. package/src/adapter/repositories/RealSleeper.ts +12 -0
  114. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +153 -0
  115. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +83 -0
  116. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +220 -0
  117. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +139 -0
  118. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +217 -0
  119. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +145 -0
  120. package/src/domain/entities/LiveSessionActivitySnapshot.ts +12 -0
  121. package/src/domain/entities/LiveSessionOutputActivity.ts +4 -0
  122. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +52 -0
  123. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +39 -0
  124. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +2 -0
  125. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +40 -0
  126. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +612 -0
  127. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +223 -0
  128. package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +36 -0
  129. package/src/domain/usecases/OauthTokenSelectUseCase.ts +12 -0
  130. package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +5 -0
  131. package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +7 -0
  132. package/src/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.ts +7 -0
  133. package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +6 -0
  134. package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +11 -0
  135. package/src/domain/usecases/adapter-interfaces/Sleeper.ts +3 -0
  136. package/src/domain/usecases/adapter-interfaces/SubAgentProcessLister.ts +8 -0
  137. package/src/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.ts +3 -0
  138. package/src/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.ts +3 -0
  139. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  140. package/types/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.d.ts.map +1 -1
  141. package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
  142. package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
  143. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +34 -0
  144. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -0
  145. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +1 -0
  146. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  147. package/types/adapter/proxy/RateLimitCache.d.ts +3 -0
  148. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  149. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  150. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +15 -0
  151. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -0
  152. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +10 -0
  153. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -0
  154. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts +9 -0
  155. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts.map +1 -0
  156. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts +7 -0
  157. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  158. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +5 -0
  159. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -0
  160. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts +8 -0
  161. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts.map +1 -0
  162. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts +12 -0
  163. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -0
  164. package/types/adapter/repositories/RealSleeper.d.ts +5 -0
  165. package/types/adapter/repositories/RealSleeper.d.ts.map +1 -0
  166. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +13 -0
  167. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -0
  168. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +10 -0
  169. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -0
  170. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +12 -0
  171. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -0
  172. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +12 -0
  173. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -0
  174. package/types/domain/entities/LiveSessionOutputActivity.d.ts +5 -0
  175. package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -0
  176. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +7 -0
  177. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -0
  178. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +41 -0
  179. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -0
  180. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +2 -0
  181. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
  182. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +4 -0
  183. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -0
  184. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +5 -0
  185. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -0
  186. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts +5 -0
  187. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts.map +1 -0
  188. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +6 -0
  189. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -0
  190. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +6 -0
  191. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -0
  192. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts +4 -0
  193. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts.map +1 -0
  194. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts +8 -0
  195. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts.map +1 -0
  196. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts +4 -0
  197. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts.map +1 -0
  198. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts +4 -0
  199. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  200. package/bin/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
  201. package/src/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
@@ -0,0 +1,51 @@
1
+ import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
2
+ import { SilentSessionMessageComposer } from '../../domain/usecases/adapter-interfaces/SilentSessionMessageComposer';
3
+
4
+ export type SilentSessionMessageTemplates = {
5
+ mainStalledMessage: string | null;
6
+ subAgentMessageHeader: string | null;
7
+ subAgentMessageFooter: string | null;
8
+ };
9
+
10
+ const formatMinutes = (seconds: number): string => {
11
+ const minutes = Math.floor(seconds / 60);
12
+ return `${minutes}m`;
13
+ };
14
+
15
+ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMessageComposer {
16
+ constructor(
17
+ private readonly templates: SilentSessionMessageTemplates,
18
+ private readonly fallback: SilentSessionMessageComposer,
19
+ ) {}
20
+
21
+ composeMainStalledSection = (mainSilentSeconds: number): string => {
22
+ if (this.templates.mainStalledMessage === null) {
23
+ return this.fallback.composeMainStalledSection(mainSilentSeconds);
24
+ }
25
+ return this.templates.mainStalledMessage;
26
+ };
27
+
28
+ composeSubAgentSection = (subAgents: SubAgentActivity[]): string => {
29
+ if (
30
+ this.templates.subAgentMessageHeader === null &&
31
+ this.templates.subAgentMessageFooter === null
32
+ ) {
33
+ return this.fallback.composeSubAgentSection(subAgents);
34
+ }
35
+ const lines = subAgents.map(
36
+ (subAgent) =>
37
+ `- ${subAgent.label}: silent for ${formatMinutes(
38
+ subAgent.silentSeconds,
39
+ )}, running for ${formatMinutes(subAgent.runningSeconds)}`,
40
+ );
41
+ const sections: string[] = [];
42
+ if (this.templates.subAgentMessageHeader !== null) {
43
+ sections.push(this.templates.subAgentMessageHeader);
44
+ }
45
+ sections.push(...lines);
46
+ if (this.templates.subAgentMessageFooter !== null) {
47
+ sections.push(this.templates.subAgentMessageFooter);
48
+ }
49
+ return sections.join('\n');
50
+ };
51
+ }
@@ -0,0 +1,92 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { FileSystemSessionOutputActivityRepository } from './FileSystemSessionOutputActivityRepository';
5
+
6
+ describe('FileSystemSessionOutputActivityRepository', () => {
7
+ let rootDirectory: string;
8
+
9
+ beforeEach(() => {
10
+ rootDirectory = fs.mkdtempSync(
11
+ path.join(os.tmpdir(), 'session-output-activity-'),
12
+ );
13
+ });
14
+
15
+ afterEach(() => {
16
+ fs.rmSync(rootDirectory, { force: true, recursive: true });
17
+ });
18
+
19
+ const writeOutputFileWithMtime = (
20
+ sessionName: string,
21
+ epochSeconds: number,
22
+ ): void => {
23
+ const fileName = sessionName.replace(/\//g, '_');
24
+ const filePath = path.join(rootDirectory, fileName);
25
+ fs.writeFileSync(filePath, 'output', 'utf8');
26
+ fs.utimesSync(filePath, epochSeconds, epochSeconds);
27
+ };
28
+
29
+ it('returns the latest output epoch derived from each session output file mtime', async () => {
30
+ const sessionName = 'https_//github_com/owner/repo/issues/9';
31
+ writeOutputFileWithMtime(sessionName, 1700000000);
32
+ const repository = new FileSystemSessionOutputActivityRepository(
33
+ rootDirectory,
34
+ );
35
+
36
+ const result = await repository.listSessionOutputActivities([sessionName]);
37
+
38
+ expect(result).toEqual([
39
+ { sessionName, lastOutputEpochSeconds: 1700000000 },
40
+ ]);
41
+ });
42
+
43
+ it('omits sessions that have no output file', async () => {
44
+ const presentSessionName = 'https_//github_com/owner/repo/issues/9';
45
+ const missingSessionName = 'https_//github_com/owner/repo/issues/10';
46
+ writeOutputFileWithMtime(presentSessionName, 1700000000);
47
+ const repository = new FileSystemSessionOutputActivityRepository(
48
+ rootDirectory,
49
+ );
50
+
51
+ const result = await repository.listSessionOutputActivities([
52
+ presentSessionName,
53
+ missingSessionName,
54
+ ]);
55
+
56
+ expect(result).toEqual([
57
+ { sessionName: presentSessionName, lastOutputEpochSeconds: 1700000000 },
58
+ ]);
59
+ });
60
+
61
+ it('returns an empty list when the root directory does not exist', async () => {
62
+ const repository = new FileSystemSessionOutputActivityRepository(
63
+ path.join(rootDirectory, 'does-not-exist'),
64
+ );
65
+
66
+ const result = await repository.listSessionOutputActivities([
67
+ 'https_//github_com/owner/repo/issues/9',
68
+ ]);
69
+
70
+ expect(result).toEqual([]);
71
+ });
72
+
73
+ it('returns an empty list when the root directory is null', async () => {
74
+ const repository = new FileSystemSessionOutputActivityRepository(null);
75
+
76
+ const result = await repository.listSessionOutputActivities([
77
+ 'https_//github_com/owner/repo/issues/9',
78
+ ]);
79
+
80
+ expect(result).toEqual([]);
81
+ });
82
+
83
+ it('returns an empty list when no session names are requested', async () => {
84
+ const repository = new FileSystemSessionOutputActivityRepository(
85
+ rootDirectory,
86
+ );
87
+
88
+ const result = await repository.listSessionOutputActivities([]);
89
+
90
+ expect(result).toEqual([]);
91
+ });
92
+ });
@@ -0,0 +1,48 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { LiveSessionOutputActivity } from '../../domain/entities/LiveSessionOutputActivity';
4
+ import { SessionOutputActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionOutputActivityRepository';
5
+
6
+ export class FileSystemSessionOutputActivityRepository implements SessionOutputActivityRepository {
7
+ constructor(private readonly rootDirectory: string | null) {}
8
+
9
+ listSessionOutputActivities = async (
10
+ sessionNames: string[],
11
+ ): Promise<LiveSessionOutputActivity[]> => {
12
+ if (this.rootDirectory === null) {
13
+ return [];
14
+ }
15
+ const activities: LiveSessionOutputActivity[] = [];
16
+ for (const sessionName of sessionNames) {
17
+ const lastOutputEpochSeconds =
18
+ this.readLastOutputEpochSeconds(sessionName);
19
+ if (lastOutputEpochSeconds !== null) {
20
+ activities.push({ sessionName, lastOutputEpochSeconds });
21
+ }
22
+ }
23
+ return activities;
24
+ };
25
+
26
+ private readLastOutputEpochSeconds = (sessionName: string): number | null => {
27
+ if (this.rootDirectory === null) {
28
+ return null;
29
+ }
30
+ const filePath = path.join(
31
+ this.rootDirectory,
32
+ this.toOutputFileName(sessionName),
33
+ );
34
+ let stats: fs.Stats;
35
+ try {
36
+ stats = fs.statSync(filePath);
37
+ } catch {
38
+ return null;
39
+ }
40
+ if (!stats.isFile()) {
41
+ return null;
42
+ }
43
+ return Math.floor(stats.mtimeMs / 1000);
44
+ };
45
+
46
+ private toOutputFileName = (sessionName: string): string =>
47
+ sessionName.replace(/\//g, '_');
48
+ }
@@ -0,0 +1,56 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { FileSystemSubAgentSilentSecondsResolver } from './FileSystemSubAgentSilentSecondsResolver';
5
+
6
+ const NOW = new Date('2026-06-26T00:00:00.000Z');
7
+ const NOW_EPOCH_SECONDS = Math.floor(NOW.getTime() / 1000);
8
+
9
+ describe('FileSystemSubAgentSilentSecondsResolver', () => {
10
+ let rootDirectory: string;
11
+
12
+ beforeEach(() => {
13
+ rootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'subagent-out-'));
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(rootDirectory, { force: true, recursive: true });
18
+ });
19
+
20
+ it('returns 0 when the root directory is not configured', () => {
21
+ const resolver = new FileSystemSubAgentSilentSecondsResolver(null, NOW);
22
+ expect(resolver.resolveSilentSeconds('task-a')).toBe(0);
23
+ });
24
+
25
+ it('returns 0 when the output file does not exist', () => {
26
+ const resolver = new FileSystemSubAgentSilentSecondsResolver(
27
+ rootDirectory,
28
+ NOW,
29
+ );
30
+ expect(resolver.resolveSilentSeconds('task-a')).toBe(0);
31
+ });
32
+
33
+ it('returns the seconds since the output file was last modified', () => {
34
+ const filePath = path.join(rootDirectory, 'task-a');
35
+ fs.writeFileSync(filePath, 'output', 'utf8');
36
+ const silentEpoch = NOW_EPOCH_SECONDS - 360;
37
+ fs.utimesSync(filePath, silentEpoch, silentEpoch);
38
+ const resolver = new FileSystemSubAgentSilentSecondsResolver(
39
+ rootDirectory,
40
+ NOW,
41
+ );
42
+ expect(resolver.resolveSilentSeconds('task-a')).toBe(360);
43
+ });
44
+
45
+ it('replaces slashes in the label when resolving the file name', () => {
46
+ const filePath = path.join(rootDirectory, 'group_task-a');
47
+ fs.writeFileSync(filePath, 'output', 'utf8');
48
+ const silentEpoch = NOW_EPOCH_SECONDS - 120;
49
+ fs.utimesSync(filePath, silentEpoch, silentEpoch);
50
+ const resolver = new FileSystemSubAgentSilentSecondsResolver(
51
+ rootDirectory,
52
+ NOW,
53
+ );
54
+ expect(resolver.resolveSilentSeconds('group/task-a')).toBe(120);
55
+ });
56
+ });
@@ -0,0 +1,32 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { SubAgentSilentSecondsResolver } from '../../domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver';
4
+
5
+ export class FileSystemSubAgentSilentSecondsResolver implements SubAgentSilentSecondsResolver {
6
+ constructor(
7
+ private readonly rootDirectory: string | null,
8
+ private readonly now: Date,
9
+ ) {}
10
+
11
+ resolveSilentSeconds = (label: string): number => {
12
+ if (this.rootDirectory === null) {
13
+ return 0;
14
+ }
15
+ const filePath = path.join(this.rootDirectory, this.toFileName(label));
16
+ let stats: fs.Stats;
17
+ try {
18
+ stats = fs.statSync(filePath);
19
+ } catch {
20
+ return 0;
21
+ }
22
+ if (!stats.isFile()) {
23
+ return 0;
24
+ }
25
+ const lastOutputEpochSeconds = Math.floor(stats.mtimeMs / 1000);
26
+ const nowEpochSeconds = Math.floor(this.now.getTime() / 1000);
27
+ const silentSeconds = nowEpochSeconds - lastOutputEpochSeconds;
28
+ return silentSeconds > 0 ? silentSeconds : 0;
29
+ };
30
+
31
+ private toFileName = (label: string): string => label.replace(/\//g, '_');
32
+ }
@@ -0,0 +1,32 @@
1
+ import * as path from 'path';
2
+ import { FileSystemSubAgentTranscriptDirectoryResolver } from './FileSystemSubAgentTranscriptDirectoryResolver';
3
+
4
+ describe('FileSystemSubAgentTranscriptDirectoryResolver', () => {
5
+ it('builds the subagents directory under the session-specific directory', () => {
6
+ const resolver = new FileSystemSubAgentTranscriptDirectoryResolver(
7
+ '/var/transcripts',
8
+ );
9
+
10
+ const result = resolver.resolveSubAgentsDirectory(
11
+ 'https_//github_com/owner/repo/issues/9',
12
+ );
13
+
14
+ expect(result).toBe(
15
+ path.join(
16
+ '/var/transcripts',
17
+ 'https_//github_com_owner_repo_issues_9'.replace(/\//g, '_'),
18
+ 'subagents',
19
+ ),
20
+ );
21
+ });
22
+
23
+ it('returns null when the root directory is null', () => {
24
+ const resolver = new FileSystemSubAgentTranscriptDirectoryResolver(null);
25
+
26
+ const result = resolver.resolveSubAgentsDirectory(
27
+ 'https_//github_com/owner/repo/issues/9',
28
+ );
29
+
30
+ expect(result).toBeNull();
31
+ });
32
+ });
@@ -0,0 +1,17 @@
1
+ import * as path from 'path';
2
+ import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
3
+
4
+ export class FileSystemSubAgentTranscriptDirectoryResolver implements SubAgentTranscriptDirectoryResolver {
5
+ constructor(private readonly rootDirectory: string | null) {}
6
+
7
+ resolveSubAgentsDirectory = (sessionName: string): string | null => {
8
+ if (this.rootDirectory === null) {
9
+ return null;
10
+ }
11
+ return path.join(
12
+ this.rootDirectory,
13
+ sessionName.replace(/\//g, '_'),
14
+ 'subagents',
15
+ );
16
+ };
17
+ }
@@ -0,0 +1,12 @@
1
+ import { NoUnansweredOwnerCallStatusProvider } from './NoUnansweredOwnerCallStatusProvider';
2
+
3
+ describe('NoUnansweredOwnerCallStatusProvider', () => {
4
+ it('always reports no session as having an unanswered owner call', async () => {
5
+ const provider = new NoUnansweredOwnerCallStatusProvider();
6
+ const result = await provider.listSessionNamesWithUnansweredOwnerCall([
7
+ 'session-a',
8
+ 'session-b',
9
+ ]);
10
+ expect(result.size).toBe(0);
11
+ });
12
+ });
@@ -0,0 +1,9 @@
1
+ import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
2
+
3
+ export class NoUnansweredOwnerCallStatusProvider implements OwnerCallStatusProvider {
4
+ listSessionNamesWithUnansweredOwnerCall = (
5
+ _sessionNames: string[],
6
+ ): Promise<Set<string>> => {
7
+ return Promise.resolve(new Set<string>());
8
+ };
9
+ }
@@ -0,0 +1,43 @@
1
+ import { NodeSubAgentProcessLister } from './NodeSubAgentProcessLister';
2
+ import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
3
+
4
+ type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
5
+
6
+ const createRunner = (
7
+ stdout: string,
8
+ exitCode = 0,
9
+ ): Mocked<LocalCommandRunner> => ({
10
+ runCommand: jest.fn().mockResolvedValue({ stdout, stderr: '', exitCode }),
11
+ });
12
+
13
+ describe('NodeSubAgentProcessLister', () => {
14
+ it('parses elapsed seconds and command line from ps output', async () => {
15
+ const runner = createRunner(
16
+ ' 1200 worker session=s1 label=task-a\n 30 another --flag value\n',
17
+ );
18
+ const lister = new NodeSubAgentProcessLister(runner);
19
+ const processes = await lister.listProcesses();
20
+ expect(processes).toEqual([
21
+ { commandLine: 'worker session=s1 label=task-a', elapsedSeconds: 1200 },
22
+ { commandLine: 'another --flag value', elapsedSeconds: 30 },
23
+ ]);
24
+ expect(runner.runCommand.mock.calls[0]).toEqual([
25
+ 'ps',
26
+ ['-eo', 'etimes=,args='],
27
+ ]);
28
+ });
29
+
30
+ it('returns an empty array when ps exits non-zero', async () => {
31
+ const runner = createRunner('', 1);
32
+ const lister = new NodeSubAgentProcessLister(runner);
33
+ expect(await lister.listProcesses()).toEqual([]);
34
+ });
35
+
36
+ it('skips blank lines and lines without a command', async () => {
37
+ const runner = createRunner('\n 1200 \n 500 valid command\n');
38
+ const lister = new NodeSubAgentProcessLister(runner);
39
+ expect(await lister.listProcesses()).toEqual([
40
+ { commandLine: 'valid command', elapsedSeconds: 500 },
41
+ ]);
42
+ });
43
+ });
@@ -0,0 +1,40 @@
1
+ import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
+ import {
3
+ SubAgentProcess,
4
+ SubAgentProcessLister,
5
+ } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
6
+
7
+ export class NodeSubAgentProcessLister implements SubAgentProcessLister {
8
+ constructor(private readonly localCommandRunner: LocalCommandRunner) {}
9
+
10
+ listProcesses = async (): Promise<SubAgentProcess[]> => {
11
+ const { stdout, exitCode } = await this.localCommandRunner.runCommand(
12
+ 'ps',
13
+ ['-eo', 'etimes=,args='],
14
+ );
15
+ if (exitCode !== 0) {
16
+ return [];
17
+ }
18
+ const processes: SubAgentProcess[] = [];
19
+ for (const line of stdout.split('\n')) {
20
+ const trimmed = line.trim();
21
+ if (trimmed.length === 0) {
22
+ continue;
23
+ }
24
+ const separatorIndex = trimmed.indexOf(' ');
25
+ if (separatorIndex < 0) {
26
+ continue;
27
+ }
28
+ const elapsedSeconds = Number(trimmed.slice(0, separatorIndex));
29
+ if (!Number.isFinite(elapsedSeconds)) {
30
+ continue;
31
+ }
32
+ const commandLine = trimmed.slice(separatorIndex + 1).trim();
33
+ if (commandLine.length === 0) {
34
+ continue;
35
+ }
36
+ processes.push({ commandLine, elapsedSeconds });
37
+ }
38
+ return processes;
39
+ };
40
+ }
@@ -0,0 +1,109 @@
1
+ import { ProcessListSessionSubAgentActivityRepository } from './ProcessListSessionSubAgentActivityRepository';
2
+ import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
3
+ import { SubAgentSilentSecondsResolver } from '../../domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver';
4
+
5
+ type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
6
+
7
+ const createLister = (
8
+ processes: { commandLine: string; elapsedSeconds: number }[],
9
+ ): Mocked<SubAgentProcessLister> => ({
10
+ listProcesses: jest.fn().mockResolvedValue(processes),
11
+ });
12
+
13
+ const createResolver = (
14
+ silentSeconds: number,
15
+ ): Mocked<SubAgentSilentSecondsResolver> => ({
16
+ resolveSilentSeconds: jest.fn().mockReturnValue(silentSeconds),
17
+ });
18
+
19
+ describe('ProcessListSessionSubAgentActivityRepository', () => {
20
+ it('returns an empty map when the match pattern is not configured', async () => {
21
+ const repository = new ProcessListSessionSubAgentActivityRepository(
22
+ null,
23
+ createLister([
24
+ { commandLine: 'anything session=s1', elapsedSeconds: 10 },
25
+ ]),
26
+ createResolver(0),
27
+ );
28
+ const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
29
+ expect(result.size).toBe(0);
30
+ });
31
+
32
+ it('returns an empty map when the match pattern is an empty string', async () => {
33
+ const repository = new ProcessListSessionSubAgentActivityRepository(
34
+ '',
35
+ createLister([
36
+ { commandLine: 'anything session=s1', elapsedSeconds: 10 },
37
+ ]),
38
+ createResolver(0),
39
+ );
40
+ const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
41
+ expect(result.size).toBe(0);
42
+ });
43
+
44
+ it('associates a matched process to a monitored session using the session capture group', async () => {
45
+ const repository = new ProcessListSessionSubAgentActivityRepository(
46
+ 'session=(?<session>[^ ]+) label=(?<label>[^ ]+)',
47
+ createLister([
48
+ {
49
+ commandLine: 'worker session=s1 label=task-a',
50
+ elapsedSeconds: 1200,
51
+ },
52
+ ]),
53
+ createResolver(180),
54
+ );
55
+ const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
56
+ expect(result.get('s1')).toEqual([
57
+ { label: 'task-a', silentSeconds: 180, runningSeconds: 1200 },
58
+ ]);
59
+ });
60
+
61
+ it('ignores processes whose captured session is not monitored', async () => {
62
+ const repository = new ProcessListSessionSubAgentActivityRepository(
63
+ 'session=(?<session>[^ ]+)',
64
+ createLister([
65
+ { commandLine: 'worker session=other', elapsedSeconds: 1200 },
66
+ ]),
67
+ createResolver(0),
68
+ );
69
+ const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
70
+ expect(result.size).toBe(0);
71
+ });
72
+
73
+ it('falls back to the session name as the label when no label group is captured', async () => {
74
+ const repository = new ProcessListSessionSubAgentActivityRepository(
75
+ 'session=(?<session>[^ ]+)',
76
+ createLister([{ commandLine: 'worker session=s1', elapsedSeconds: 30 }]),
77
+ createResolver(0),
78
+ );
79
+ const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
80
+ expect(result.get('s1')).toEqual([
81
+ { label: 's1', silentSeconds: 0, runningSeconds: 30 },
82
+ ]);
83
+ });
84
+
85
+ it('groups multiple matched processes under the same session', async () => {
86
+ const repository = new ProcessListSessionSubAgentActivityRepository(
87
+ 'session=(?<session>[^ ]+) label=(?<label>[^ ]+)',
88
+ createLister([
89
+ { commandLine: 'worker session=s1 label=task-a', elapsedSeconds: 10 },
90
+ { commandLine: 'worker session=s1 label=task-b', elapsedSeconds: 20 },
91
+ ]),
92
+ createResolver(0),
93
+ );
94
+ const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
95
+ expect(result.get('s1')).toHaveLength(2);
96
+ });
97
+
98
+ it('ignores processes that do not match the pattern', async () => {
99
+ const repository = new ProcessListSessionSubAgentActivityRepository(
100
+ 'session=(?<session>[^ ]+)',
101
+ createLister([
102
+ { commandLine: 'unrelated process', elapsedSeconds: 1200 },
103
+ ]),
104
+ createResolver(0),
105
+ );
106
+ const result = await repository.listSubAgentActivitiesBySessionName(['s1']);
107
+ expect(result.size).toBe(0);
108
+ });
109
+ });
@@ -0,0 +1,56 @@
1
+ import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
2
+ import { SessionSubAgentActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository';
3
+ import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
4
+ import { SubAgentSilentSecondsResolver } from '../../domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver';
5
+
6
+ export class ProcessListSessionSubAgentActivityRepository implements SessionSubAgentActivityRepository {
7
+ private readonly matchRegExp: RegExp | null;
8
+
9
+ constructor(
10
+ matchPattern: string | null,
11
+ private readonly processLister: SubAgentProcessLister,
12
+ private readonly silentSecondsResolver: SubAgentSilentSecondsResolver,
13
+ ) {
14
+ this.matchRegExp =
15
+ matchPattern === null || matchPattern.length === 0
16
+ ? null
17
+ : new RegExp(matchPattern);
18
+ }
19
+
20
+ listSubAgentActivitiesBySessionName = async (
21
+ sessionNames: string[],
22
+ ): Promise<Map<string, SubAgentActivity[]>> => {
23
+ const result = new Map<string, SubAgentActivity[]>();
24
+ if (this.matchRegExp === null) {
25
+ return result;
26
+ }
27
+ const monitoredSessionNames = new Set(sessionNames);
28
+ const processes = await this.processLister.listProcesses();
29
+ for (const process of processes) {
30
+ const match = this.matchRegExp.exec(process.commandLine);
31
+ if (match === null || match.groups === undefined) {
32
+ continue;
33
+ }
34
+ const sessionName = match.groups.session;
35
+ if (
36
+ sessionName === undefined ||
37
+ !monitoredSessionNames.has(sessionName)
38
+ ) {
39
+ continue;
40
+ }
41
+ const label = match.groups.label ?? sessionName;
42
+ const activity: SubAgentActivity = {
43
+ label,
44
+ silentSeconds: this.silentSecondsResolver.resolveSilentSeconds(label),
45
+ runningSeconds: process.elapsedSeconds,
46
+ };
47
+ const existing = result.get(sessionName);
48
+ if (existing === undefined) {
49
+ result.set(sessionName, [activity]);
50
+ } else {
51
+ existing.push(activity);
52
+ }
53
+ }
54
+ return result;
55
+ };
56
+ }
@@ -0,0 +1,19 @@
1
+ import { RealSleeper } from './RealSleeper';
2
+
3
+ describe('RealSleeper', () => {
4
+ it('resolves after roughly the requested delay', async () => {
5
+ const sleeper = new RealSleeper();
6
+ const start = Date.now();
7
+ await sleeper.sleep(30);
8
+ const elapsed = Date.now() - start;
9
+ expect(elapsed).toBeGreaterThanOrEqual(20);
10
+ });
11
+
12
+ it('resolves immediately for a non-positive delay', async () => {
13
+ const sleeper = new RealSleeper();
14
+ const start = Date.now();
15
+ await sleeper.sleep(0);
16
+ const elapsed = Date.now() - start;
17
+ expect(elapsed).toBeLessThan(20);
18
+ });
19
+ });
@@ -0,0 +1,12 @@
1
+ import { Sleeper } from '../../domain/usecases/adapter-interfaces/Sleeper';
2
+
3
+ export class RealSleeper implements Sleeper {
4
+ sleep = (milliseconds: number): Promise<void> => {
5
+ if (milliseconds <= 0) {
6
+ return Promise.resolve();
7
+ }
8
+ return new Promise((resolve) => {
9
+ setTimeout(resolve, milliseconds);
10
+ });
11
+ };
12
+ }