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
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-BpccigBS.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CO-f_j4f.css">
7
+ <script type="module" crossorigin src="/assets/index-CT3t4Yje.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BPH6W1yn.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -182,7 +182,7 @@ describe('webServer integration', () => {
182
182
  inTmuxDataDir: null,
183
183
  dashboardDir: null,
184
184
  dashboardDataDir: null,
185
- dashboardProjectCodes: [],
185
+ dashboardProjectNames: [],
186
186
  port: 0,
187
187
  });
188
188
  const address = server.address();
@@ -201,7 +201,7 @@ describe('webServer integration', () => {
201
201
  inTmuxDataDir: null,
202
202
  dashboardDir: null,
203
203
  dashboardDataDir: null,
204
- dashboardProjectCodes: [],
204
+ dashboardProjectNames: [],
205
205
  port: 0,
206
206
  });
207
207
  try {
@@ -229,7 +229,7 @@ describe('webServer integration', () => {
229
229
  inTmuxDataDir: null,
230
230
  dashboardDir: null,
231
231
  dashboardDataDir: null,
232
- dashboardProjectCodes: [],
232
+ dashboardProjectNames: [],
233
233
  port: 0,
234
234
  });
235
235
  try {
@@ -261,7 +261,7 @@ describe('webServer integration', () => {
261
261
  inTmuxDataDir: null,
262
262
  dashboardDir: null,
263
263
  dashboardDataDir: null,
264
- dashboardProjectCodes: [],
264
+ dashboardProjectNames: [],
265
265
  port: 0,
266
266
  });
267
267
  try {
@@ -295,7 +295,7 @@ describe('webServer integration', () => {
295
295
  inTmuxDataDir: null,
296
296
  dashboardDir: null,
297
297
  dashboardDataDir: null,
298
- dashboardProjectCodes: [],
298
+ dashboardProjectNames: [],
299
299
  port: 0,
300
300
  });
301
301
  try {
@@ -326,7 +326,7 @@ describe('webServer integration', () => {
326
326
  inTmuxDataDir: null,
327
327
  dashboardDir: null,
328
328
  dashboardDataDir: null,
329
- dashboardProjectCodes: [],
329
+ dashboardProjectNames: [],
330
330
  port: 0,
331
331
  });
332
332
  try {
@@ -351,7 +351,7 @@ describe('webServer integration', () => {
351
351
  inTmuxDataDir: null,
352
352
  dashboardDir: null,
353
353
  dashboardDataDir: null,
354
- dashboardProjectCodes: [],
354
+ dashboardProjectNames: [],
355
355
  port: 0,
356
356
  });
357
357
  try {
@@ -379,7 +379,7 @@ describe('webServer integration', () => {
379
379
  inTmuxDataDir: null,
380
380
  dashboardDir: null,
381
381
  dashboardDataDir: null,
382
- dashboardProjectCodes: [],
382
+ dashboardProjectNames: [],
383
383
  port: 0,
384
384
  });
385
385
  try {
@@ -519,7 +519,7 @@ describe('webServer new routes integration', () => {
519
519
  inTmuxDataDir: null,
520
520
  dashboardDir: null,
521
521
  dashboardDataDir: null,
522
- dashboardProjectCodes: [],
522
+ dashboardProjectNames: [],
523
523
  port: 0,
524
524
  });
525
525
  try {
@@ -558,7 +558,7 @@ describe('webServer new routes integration', () => {
558
558
  inTmuxDataDir: null,
559
559
  dashboardDir: null,
560
560
  dashboardDataDir: null,
561
- dashboardProjectCodes: [],
561
+ dashboardProjectNames: [],
562
562
  issueRepository,
563
563
  issueTitleStateCache: new IssueTitleStateCache(),
564
564
  port: 0,
@@ -594,7 +594,7 @@ describe('webServer new routes integration', () => {
594
594
  inTmuxDataDir: null,
595
595
  dashboardDir: null,
596
596
  dashboardDataDir: null,
597
- dashboardProjectCodes: [],
597
+ dashboardProjectNames: [],
598
598
  issueRepository,
599
599
  resolveProject: async (pjcode) =>
600
600
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -645,7 +645,7 @@ describe('webServer new routes integration', () => {
645
645
  inTmuxDataDir: null,
646
646
  dashboardDir: null,
647
647
  dashboardDataDir: null,
648
- dashboardProjectCodes: [],
648
+ dashboardProjectNames: [],
649
649
  issueRepository,
650
650
  resolveProject: async (pjcode) =>
651
651
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -693,7 +693,7 @@ describe('webServer new routes integration', () => {
693
693
  inTmuxDataDir: null,
694
694
  dashboardDir: null,
695
695
  dashboardDataDir: null,
696
- dashboardProjectCodes: [],
696
+ dashboardProjectNames: [],
697
697
  issueRepository,
698
698
  resolveProject: async (pjcode) =>
699
699
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -731,7 +731,7 @@ describe('webServer new routes integration', () => {
731
731
  inTmuxDataDir: null,
732
732
  dashboardDir: null,
733
733
  dashboardDataDir: null,
734
- dashboardProjectCodes: [],
734
+ dashboardProjectNames: [],
735
735
  issueRepository,
736
736
  resolveProject: async (pjcode) =>
737
737
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -784,7 +784,7 @@ describe('webServer new routes integration', () => {
784
784
  inTmuxDataDir: null,
785
785
  dashboardDir: null,
786
786
  dashboardDataDir: null,
787
- dashboardProjectCodes: [],
787
+ dashboardProjectNames: [],
788
788
  port: 0,
789
789
  });
790
790
  try {
@@ -932,7 +932,7 @@ describe('webServer flat in-tmux-by-human route integration', () => {
932
932
  inTmuxDataDir,
933
933
  dashboardDir: null,
934
934
  dashboardDataDir: null,
935
- dashboardProjectCodes: [],
935
+ dashboardProjectNames: [],
936
936
  port: 0,
937
937
  });
938
938
  return { server, tmpDir, inTmuxDataDir };
@@ -1048,7 +1048,7 @@ describe('webServer flat in-tmux-by-human route integration', () => {
1048
1048
  inTmuxDataDir: null,
1049
1049
  dashboardDir: null,
1050
1050
  dashboardDataDir: null,
1051
- dashboardProjectCodes: [],
1051
+ dashboardProjectNames: [],
1052
1052
  port: 0,
1053
1053
  });
1054
1054
  try {
@@ -1121,9 +1121,9 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1121
1121
  const writeDataFiles = (dataDir: string): void => {
1122
1122
  fs.mkdirSync(path.join(dataDir, 'projects'), { recursive: true });
1123
1123
  fs.writeFileSync(
1124
- path.join(dataDir, 'projects', 'um.json'),
1124
+ path.join(dataDir, 'projects', 'umino.json'),
1125
1125
  JSON.stringify({
1126
- pjcode: 'um',
1126
+ pjcode: 'umino',
1127
1127
  capturedAt: '2026-06-26T00:00:00.000Z',
1128
1128
  unread: 3,
1129
1129
  todo: 1,
@@ -1136,9 +1136,9 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1136
1136
  }),
1137
1137
  );
1138
1138
  fs.writeFileSync(
1139
- path.join(dataDir, 'projects', 'xc.json'),
1139
+ path.join(dataDir, 'projects', 'xcare.json'),
1140
1140
  JSON.stringify({
1141
- pjcode: 'xc',
1141
+ pjcode: 'xcare',
1142
1142
  capturedAt: '2026-06-26T00:00:00.000Z',
1143
1143
  unread: 0,
1144
1144
  todo: 0,
@@ -1209,7 +1209,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1209
1209
  inTmuxDataDir: null,
1210
1210
  dashboardDir: overrides.dashboardDir,
1211
1211
  dashboardDataDir: overrides.dashboardDataDir,
1212
- dashboardProjectCodes: ['um', 'xc'],
1212
+ dashboardProjectNames: ['umino', 'xcare'],
1213
1213
  port: 0,
1214
1214
  });
1215
1215
  return { server, tmpDir };
@@ -1453,7 +1453,7 @@ describe('webServer image proxy', () => {
1453
1453
  inTmuxDataDir: null,
1454
1454
  dashboardDir: null,
1455
1455
  dashboardDataDir: null,
1456
- dashboardProjectCodes: [],
1456
+ dashboardProjectNames: [],
1457
1457
  githubToken: token,
1458
1458
  imageFetcher: fetcher,
1459
1459
  port: 0,
@@ -30,10 +30,11 @@ import {
30
30
  composeDashboardText,
31
31
  dashboardComposeFilesPresent,
32
32
  } from './dashboardComposeService';
33
+ import { DASHBOARD_PROJECT_NAMES } from '../../../domain/usecases/dashboard/DashboardProjectCode';
33
34
 
34
35
  export const DEFAULT_WEB_PORT = 9981;
35
36
 
36
- export const DEFAULT_DASHBOARD_PROJECT_CODES = ['um', 'xm', 'xc', 'ut'];
37
+ export const DEFAULT_DASHBOARD_PROJECT_NAMES = DASHBOARD_PROJECT_NAMES;
37
38
 
38
39
  export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
39
40
 
@@ -165,7 +166,7 @@ export type WebServerOptions = {
165
166
  inTmuxDataDir: string | null;
166
167
  dashboardDir: string | null;
167
168
  dashboardDataDir: string | null;
168
- dashboardProjectCodes: string[];
169
+ dashboardProjectNames: string[];
169
170
  githubToken?: string | null;
170
171
  imageFetcher?: ImageFetcher | null;
171
172
  issueRepository?: IssueRepository | null;
@@ -555,12 +556,12 @@ export const resolveDashboardContent = (
555
556
  options.dashboardDataDir !== null &&
556
557
  dashboardComposeFilesPresent({
557
558
  dashboardDataDir: options.dashboardDataDir,
558
- projectCodes: options.dashboardProjectCodes,
559
+ projectNames: options.dashboardProjectNames,
559
560
  })
560
561
  ) {
561
562
  const dashboardText = composeDashboardText({
562
563
  dashboardDataDir: options.dashboardDataDir,
563
- projectCodes: options.dashboardProjectCodes,
564
+ projectNames: options.dashboardProjectNames,
564
565
  });
565
566
  return Buffer.from(dashboardText, 'utf-8');
566
567
  }
@@ -9,6 +9,10 @@ import { writeTokenStatus } from './tokenStatusWriter';
9
9
  import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
10
10
  import { reconcileInTmuxByHumanSessions } from './inTmuxByHumanSessionReconciler';
11
11
  import { cleanStaleTmuxSessions } from './staleTmuxSessionCleaner';
12
+ import {
13
+ notifySilentTmuxSessions,
14
+ DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
15
+ } from './notifySilentTmuxSessions';
12
16
  import { writeRotationOrderFile } from './rotationOrderFileWriter';
13
17
  import {
14
18
  fetchProjectReadme,
@@ -61,6 +65,23 @@ import {
61
65
 
62
66
  const DEFAULT_DASHBOARD_DATA_DIR: string | null = null;
63
67
 
68
+ const readSilentSeconds = (
69
+ configValue: number | undefined,
70
+ envValue: string | undefined,
71
+ defaultValue: number,
72
+ ): number => {
73
+ if (configValue !== undefined) {
74
+ return configValue;
75
+ }
76
+ if (envValue !== undefined) {
77
+ const parsed = Number(envValue);
78
+ if (Number.isFinite(parsed)) {
79
+ return parsed;
80
+ }
81
+ }
82
+ return defaultValue;
83
+ };
84
+
64
85
  export class HandleScheduledEventUseCaseHandler {
65
86
  handle = async (
66
87
  configFilePath: string,
@@ -87,6 +108,20 @@ export class HandleScheduledEventUseCaseHandler {
87
108
  inTmuxConsoleToken?: string;
88
109
  inTmuxProjectOrder?: string[];
89
110
  inTmuxLauncherCommand?: string;
111
+ sessionOutputRootDirectory?: string;
112
+ sessionTranscriptRootDirectory?: string;
113
+ ownerCallMarker?: string;
114
+ subAgentOutputRootDirectory?: string;
115
+ subAgentProcessMatchPattern?: string;
116
+ subAgentTranscriptRootDirectory?: string;
117
+ mainSilentThresholdSeconds?: number;
118
+ subAgentSilentThresholdSeconds?: number;
119
+ subAgentRunningThresholdSeconds?: number;
120
+ silentNotificationCooldownSeconds?: number;
121
+ silentNotificationStaggerSeconds?: number;
122
+ silentMainStalledMessage?: string;
123
+ silentSubAgentMessageHeader?: string;
124
+ silentSubAgentMessageFooter?: string;
90
125
  credentials: {
91
126
  manager: {
92
127
  github: {
@@ -461,6 +496,7 @@ export class HandleScheduledEventUseCaseHandler {
461
496
  tokenListJsonPath:
462
497
  mergedInput.claudeCodeOauthTokenListJsonPath ?? null,
463
498
  issues: result.issues,
499
+ pjcode: input.projectName,
464
500
  });
465
501
  } catch (error) {
466
502
  console.error(
@@ -525,6 +561,92 @@ export class HandleScheduledEventUseCaseHandler {
525
561
  }`,
526
562
  );
527
563
  }
564
+
565
+ try {
566
+ const sessionOutputRootDirectory =
567
+ mergedInput.sessionOutputRootDirectory ??
568
+ process.env.TDPM_SESSION_OUTPUT_ROOT_DIRECTORY ??
569
+ null;
570
+ const subAgentOutputRootDirectory =
571
+ mergedInput.subAgentOutputRootDirectory ??
572
+ process.env.TDPM_SUBAGENT_OUTPUT_ROOT_DIRECTORY ??
573
+ null;
574
+ const subAgentProcessMatchPattern =
575
+ mergedInput.subAgentProcessMatchPattern ??
576
+ process.env.TDPM_SUBAGENT_PROCESS_MATCH_PATTERN ??
577
+ null;
578
+ const sessionTranscriptRootDirectory =
579
+ mergedInput.sessionTranscriptRootDirectory ??
580
+ process.env.TDPM_SESSION_TRANSCRIPT_ROOT_DIRECTORY ??
581
+ null;
582
+ const ownerCallMarker =
583
+ mergedInput.ownerCallMarker ??
584
+ process.env.TDPM_SILENT_OWNER_CALL_MARKER ??
585
+ null;
586
+ const subAgentTranscriptRootDirectory =
587
+ mergedInput.subAgentTranscriptRootDirectory ??
588
+ process.env.TDPM_SUBAGENT_TRANSCRIPT_ROOT_DIRECTORY ??
589
+ null;
590
+ await notifySilentTmuxSessions({
591
+ project: result.project,
592
+ allowCacheMinutes: mergedInput.allowIssueCacheMinutes,
593
+ issueRepository,
594
+ localCommandRunner: nodeLocalCommandRunner,
595
+ cacheRepository: localStorageCacheRepository,
596
+ sessionOutputRootDirectory,
597
+ sessionTranscriptRootDirectory,
598
+ ownerCallMarker,
599
+ subAgentOutputRootDirectory,
600
+ subAgentProcessMatchPattern,
601
+ subAgentTranscriptRootDirectory,
602
+ mainSilentThresholdSeconds: readSilentSeconds(
603
+ mergedInput.mainSilentThresholdSeconds,
604
+ process.env.TDPM_MAIN_SILENT_THRESHOLD_SECONDS,
605
+ DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.mainSilentThresholdSeconds,
606
+ ),
607
+ subAgentSilentThresholdSeconds: readSilentSeconds(
608
+ mergedInput.subAgentSilentThresholdSeconds,
609
+ process.env.TDPM_SUBAGENT_SILENT_THRESHOLD_SECONDS,
610
+ DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.subAgentSilentThresholdSeconds,
611
+ ),
612
+ subAgentRunningThresholdSeconds: readSilentSeconds(
613
+ mergedInput.subAgentRunningThresholdSeconds,
614
+ process.env.TDPM_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
615
+ DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.subAgentRunningThresholdSeconds,
616
+ ),
617
+ cooldownSeconds: readSilentSeconds(
618
+ mergedInput.silentNotificationCooldownSeconds,
619
+ process.env.TDPM_SILENT_NOTIFICATION_COOLDOWN_SECONDS,
620
+ DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.cooldownSeconds,
621
+ ),
622
+ staggerSeconds: readSilentSeconds(
623
+ mergedInput.silentNotificationStaggerSeconds,
624
+ process.env.TDPM_SILENT_NOTIFICATION_STAGGER_SECONDS,
625
+ DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.staggerSeconds,
626
+ ),
627
+ messageTemplates: {
628
+ mainStalledMessage:
629
+ mergedInput.silentMainStalledMessage ??
630
+ process.env.TDPM_SILENT_MAIN_STALLED_MESSAGE ??
631
+ null,
632
+ subAgentMessageHeader:
633
+ mergedInput.silentSubAgentMessageHeader ??
634
+ process.env.TDPM_SILENT_SUBAGENT_MESSAGE_HEADER ??
635
+ null,
636
+ subAgentMessageFooter:
637
+ mergedInput.silentSubAgentMessageFooter ??
638
+ process.env.TDPM_SILENT_SUBAGENT_MESSAGE_FOOTER ??
639
+ null,
640
+ },
641
+ now: inTmuxNow,
642
+ });
643
+ } catch (error) {
644
+ console.error(
645
+ `Failed to notify silent tmux sessions: ${
646
+ error instanceof Error ? error.message : String(error)
647
+ }`,
648
+ );
649
+ }
528
650
  }
529
651
  return result;
530
652
  };
@@ -50,6 +50,8 @@ export class InTmuxByHumanSessionTokenCountHandler {
50
50
  name,
51
51
  token,
52
52
  snapshot: null,
53
+ subscriptionDisabled: false,
54
+ unifiedRejected: false,
53
55
  }),
54
56
  );
55
57
 
@@ -85,6 +85,28 @@ describe('LiveSessionOauthTokenSelectHandler', () => {
85
85
  );
86
86
  };
87
87
 
88
+ const writeSubscriptionDisabledCache = (
89
+ token: string,
90
+ epochSeconds: number = Date.now() / 1000,
91
+ ): void => {
92
+ const payload = { subscriptionDisabledEpoch: epochSeconds };
93
+ fs.writeFileSync(
94
+ path.join(cacheDirectory, `${hashToken(token)}.json`),
95
+ JSON.stringify(payload),
96
+ );
97
+ };
98
+
99
+ const writeCacheWithHeaders = (
100
+ token: string,
101
+ headers: Record<string, string>,
102
+ ): void => {
103
+ const payload = { ts: NOW, headers, modelWeeklyLimits: {} };
104
+ fs.writeFileSync(
105
+ path.join(cacheDirectory, `${hashToken(token)}.json`),
106
+ JSON.stringify(payload),
107
+ );
108
+ };
109
+
88
110
  const buildHandler = (
89
111
  sessions: ClaudeLiveSession[],
90
112
  ): LiveSessionOauthTokenSelectHandler =>
@@ -289,4 +311,94 @@ describe('LiveSessionOauthTokenSelectHandler', () => {
289
311
  expect(output.selectedToken).toBeNull();
290
312
  expect(output.diagnostics.join('\n')).toContain('No usable token entries');
291
313
  });
314
+
315
+ it('excludes a subscription-disabled token even when it has zero live sessions', () => {
316
+ writeTokenList([
317
+ { name: 'disabled', token: 'fake-disabled' },
318
+ { name: 'active', token: 'fake-active' },
319
+ ]);
320
+ writeSubscriptionDisabledCache('fake-disabled');
321
+ writeCache('fake-active', {
322
+ fiveHourUtilization: 0.1,
323
+ fiveHourReset: NOW + HOUR,
324
+ sevenDayUtilization: 0.1,
325
+ sevenDayReset: NOW + DAY,
326
+ });
327
+
328
+ const handler = buildHandler([
329
+ { token: 'fake-active', sessionId: 'session-a' },
330
+ ]);
331
+ const output = handler.handle({
332
+ tokenListJsonPath: tokenListPath,
333
+ cacheDirectory,
334
+ nowEpochSeconds: NOW,
335
+ });
336
+
337
+ expect(output.selectedName).toBe('active');
338
+ expect(output.diagnostics.join('\n')).toContain(
339
+ 'organization has disabled Claude subscription access for Claude Code',
340
+ );
341
+ });
342
+
343
+ it('excludes a unified-rejected token even when it has zero live sessions', () => {
344
+ writeTokenList([
345
+ { name: 'rejected', token: 'fake-rejected' },
346
+ { name: 'active', token: 'fake-active' },
347
+ ]);
348
+ writeCacheWithHeaders('fake-rejected', {
349
+ 'anthropic-ratelimit-unified-status': 'rejected',
350
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
351
+ 'anthropic-ratelimit-unified-5h-reset': String(NOW + HOUR),
352
+ 'anthropic-ratelimit-unified-5h-utilization': '0.1',
353
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
354
+ 'anthropic-ratelimit-unified-7d-reset': String(NOW + DAY),
355
+ 'anthropic-ratelimit-unified-7d-utilization': '0.1',
356
+ });
357
+ writeCache('fake-active', {
358
+ fiveHourUtilization: 0.1,
359
+ fiveHourReset: NOW + HOUR,
360
+ sevenDayUtilization: 0.1,
361
+ sevenDayReset: NOW + DAY,
362
+ });
363
+
364
+ const handler = buildHandler([
365
+ { token: 'fake-active', sessionId: 'session-a' },
366
+ ]);
367
+ const output = handler.handle({
368
+ tokenListJsonPath: tokenListPath,
369
+ cacheDirectory,
370
+ nowEpochSeconds: NOW,
371
+ });
372
+
373
+ expect(output.selectedName).toBe('active');
374
+ expect(output.diagnostics.join('\n')).toContain('rejected');
375
+ });
376
+
377
+ it('returns null and a no-eligible-token diagnostic when every token is unusable', () => {
378
+ writeTokenList([
379
+ { name: 'disabled', token: 'fake-disabled' },
380
+ { name: 'rejected', token: 'fake-rejected' },
381
+ ]);
382
+ writeSubscriptionDisabledCache('fake-disabled');
383
+ writeCacheWithHeaders('fake-rejected', {
384
+ 'anthropic-ratelimit-unified-status': 'rejected',
385
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
386
+ 'anthropic-ratelimit-unified-5h-reset': String(NOW + HOUR),
387
+ 'anthropic-ratelimit-unified-5h-utilization': '0.1',
388
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
389
+ 'anthropic-ratelimit-unified-7d-reset': String(NOW + DAY),
390
+ 'anthropic-ratelimit-unified-7d-utilization': '0.1',
391
+ });
392
+
393
+ const handler = buildHandler([]);
394
+ const output = handler.handle({
395
+ tokenListJsonPath: tokenListPath,
396
+ cacheDirectory,
397
+ nowEpochSeconds: NOW,
398
+ });
399
+
400
+ expect(output.selectedToken).toBeNull();
401
+ expect(output.selectedName).toBeNull();
402
+ expect(output.diagnostics.join('\n')).toContain('No eligible token');
403
+ });
292
404
  });
@@ -75,6 +75,8 @@ export class LiveSessionOauthTokenSelectHandler {
75
75
  sevenDayUtilization: snapshot.sevenDayUtilization,
76
76
  sevenDayReset: snapshot.sevenDayReset,
77
77
  },
78
+ subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
79
+ unifiedRejected: snapshot?.unifiedRejected ?? false,
78
80
  };
79
81
  });
80
82
 
@@ -78,6 +78,28 @@ describe('OauthTokenSelectHandler', () => {
78
78
  );
79
79
  };
80
80
 
81
+ const writeSubscriptionDisabledCache = (
82
+ token: string,
83
+ epochSeconds: number = Date.now() / 1000,
84
+ ): void => {
85
+ const payload = { subscriptionDisabledEpoch: epochSeconds };
86
+ fs.writeFileSync(
87
+ path.join(cacheDirectory, `${hashToken(token)}.json`),
88
+ JSON.stringify(payload),
89
+ );
90
+ };
91
+
92
+ const writeCacheWithHeaders = (
93
+ token: string,
94
+ headers: Record<string, string>,
95
+ ): void => {
96
+ const payload = { ts: NOW, headers, modelWeeklyLimits: {} };
97
+ fs.writeFileSync(
98
+ path.join(cacheDirectory, `${hashToken(token)}.json`),
99
+ JSON.stringify(payload),
100
+ );
101
+ };
102
+
81
103
  it('selects the eligible token with the soonest 7d reset', () => {
82
104
  writeTokenList([
83
105
  { name: 'far', token: 'fake-far' },
@@ -168,6 +190,115 @@ describe('OauthTokenSelectHandler', () => {
168
190
  expect(output.diagnostics.join('\n')).toContain('No usable token entries');
169
191
  });
170
192
 
193
+ it('excludes a subscription-disabled token even when rate limits are fully free', () => {
194
+ writeTokenList([
195
+ { name: 'disabled', token: 'fake-disabled' },
196
+ { name: 'active', token: 'fake-active' },
197
+ ]);
198
+ writeSubscriptionDisabledCache('fake-disabled');
199
+ writeCache('fake-active', {
200
+ fiveHourUtilization: 0.1,
201
+ fiveHourReset: NOW + HOUR,
202
+ sevenDayUtilization: 0.1,
203
+ sevenDayReset: NOW + DAY,
204
+ });
205
+
206
+ const handler = new OauthTokenSelectHandler();
207
+ const output = handler.handle({
208
+ tokenListJsonPath: tokenListPath,
209
+ cacheDirectory,
210
+ nowEpochSeconds: NOW,
211
+ });
212
+
213
+ expect(output.selectedName).toBe('active');
214
+ expect(output.diagnostics.join('\n')).toContain(
215
+ 'organization has disabled Claude subscription access for Claude Code',
216
+ );
217
+ });
218
+
219
+ it('excludes a unified-rejected token even when rate limits are fully free', () => {
220
+ writeTokenList([
221
+ { name: 'rejected', token: 'fake-rejected' },
222
+ { name: 'active', token: 'fake-active' },
223
+ ]);
224
+ writeCacheWithHeaders('fake-rejected', {
225
+ 'anthropic-ratelimit-unified-status': 'rejected',
226
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
227
+ 'anthropic-ratelimit-unified-5h-reset': String(NOW + HOUR),
228
+ 'anthropic-ratelimit-unified-5h-utilization': '0.1',
229
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
230
+ 'anthropic-ratelimit-unified-7d-reset': String(NOW + DAY),
231
+ 'anthropic-ratelimit-unified-7d-utilization': '0.1',
232
+ });
233
+ writeCache('fake-active', {
234
+ fiveHourUtilization: 0.1,
235
+ fiveHourReset: NOW + HOUR,
236
+ sevenDayUtilization: 0.1,
237
+ sevenDayReset: NOW + DAY,
238
+ });
239
+
240
+ const handler = new OauthTokenSelectHandler();
241
+ const output = handler.handle({
242
+ tokenListJsonPath: tokenListPath,
243
+ cacheDirectory,
244
+ nowEpochSeconds: NOW,
245
+ });
246
+
247
+ expect(output.selectedName).toBe('active');
248
+ expect(output.diagnostics.join('\n')).toContain('rejected');
249
+ });
250
+
251
+ it('does not exclude a token solely because overage is disabled', () => {
252
+ writeTokenList([{ name: 'overage', token: 'fake-overage' }]);
253
+ writeCacheWithHeaders('fake-overage', {
254
+ 'anthropic-ratelimit-unified-status': 'allowed',
255
+ 'anthropic-ratelimit-unified-overage-disabled-reason': 'org_disabled',
256
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
257
+ 'anthropic-ratelimit-unified-5h-reset': String(NOW + HOUR),
258
+ 'anthropic-ratelimit-unified-5h-utilization': '0.1',
259
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
260
+ 'anthropic-ratelimit-unified-7d-reset': String(NOW + DAY),
261
+ 'anthropic-ratelimit-unified-7d-utilization': '0.1',
262
+ });
263
+
264
+ const handler = new OauthTokenSelectHandler();
265
+ const output = handler.handle({
266
+ tokenListJsonPath: tokenListPath,
267
+ cacheDirectory,
268
+ nowEpochSeconds: NOW,
269
+ });
270
+
271
+ expect(output.selectedName).toBe('overage');
272
+ expect(output.selectedToken).toBe('fake-overage');
273
+ });
274
+
275
+ it('returns null when every token is unusable (subscription-disabled or rejected)', () => {
276
+ writeTokenList([
277
+ { name: 'disabled', token: 'fake-disabled' },
278
+ { name: 'rejected', token: 'fake-rejected' },
279
+ ]);
280
+ writeSubscriptionDisabledCache('fake-disabled');
281
+ writeCacheWithHeaders('fake-rejected', {
282
+ 'anthropic-ratelimit-unified-status': 'rejected',
283
+ 'anthropic-ratelimit-unified-5h-status': 'allowed',
284
+ 'anthropic-ratelimit-unified-5h-reset': String(NOW + HOUR),
285
+ 'anthropic-ratelimit-unified-5h-utilization': '0.1',
286
+ 'anthropic-ratelimit-unified-7d-status': 'allowed',
287
+ 'anthropic-ratelimit-unified-7d-reset': String(NOW + DAY),
288
+ 'anthropic-ratelimit-unified-7d-utilization': '0.1',
289
+ });
290
+
291
+ const handler = new OauthTokenSelectHandler();
292
+ const output = handler.handle({
293
+ tokenListJsonPath: tokenListPath,
294
+ cacheDirectory,
295
+ nowEpochSeconds: NOW,
296
+ });
297
+
298
+ expect(output.selectedToken).toBeNull();
299
+ expect(output.selectedName).toBeNull();
300
+ });
301
+
171
302
  describe('resolveTokenListJsonPath', () => {
172
303
  it('prefers the explicit path over the environment variable', () => {
173
304
  process.env.CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH = '/from/env.json';
@@ -93,6 +93,8 @@ export class OauthTokenSelectHandler {
93
93
  sevenDayUtilization: snapshot.sevenDayUtilization,
94
94
  sevenDayReset: snapshot.sevenDayReset,
95
95
  },
96
+ subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
97
+ unifiedRejected: snapshot?.unifiedRejected ?? false,
96
98
  };
97
99
  });
98
100