github-issue-tower-defence-management 1.141.0 → 1.142.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +6 -6
  3. package/bin/adapter/entry-points/console/dashboardComposeService.js +23 -4
  4. package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
  6. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  7. package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js +3 -3
  8. package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js.map +1 -1
  9. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +23 -9
  10. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
  11. package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js +41 -5
  12. package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js.map +1 -1
  13. package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js +16 -1
  14. package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/console/dashboardComposeService.test.ts +82 -0
  17. package/src/adapter/entry-points/console/dashboardComposeService.ts +38 -4
  18. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +6 -0
  19. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +4 -0
  20. package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.test.ts +94 -0
  21. package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +3 -3
  22. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +368 -1
  23. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +28 -10
  24. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +31 -0
  25. package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +164 -0
  26. package/src/domain/usecases/dashboard/ComposeDashboardUseCase.ts +63 -5
  27. package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.test.ts +73 -0
  28. package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.ts +23 -0
  29. package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -1
  30. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +2 -1
  31. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  32. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +2 -1
  33. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
  34. package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts +3 -1
  35. package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts.map +1 -1
  36. package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts +6 -0
  37. package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts.map +1 -1
@@ -233,6 +233,88 @@ describe('buildComposeDashboardInput', () => {
233
233
  }
234
234
  });
235
235
 
236
+ it('reads the seven day window aggregate from token-status.json', () => {
237
+ const dataDir = makeDataDir();
238
+ try {
239
+ fs.writeFileSync(
240
+ path.join(dataDir, 'token-status.json'),
241
+ JSON.stringify({
242
+ tokens: [],
243
+ sevenDayWindowAggregate: {
244
+ usedPercent: 63.5,
245
+ includedTokenCount: 9,
246
+ totalTokenCount: 10,
247
+ },
248
+ capturedAt: 'x',
249
+ }),
250
+ );
251
+ const input = buildComposeDashboardInput({
252
+ dashboardDataDir: dataDir,
253
+ projectNames: [],
254
+ });
255
+ expect(input.sevenDayWindowAggregate).toEqual({
256
+ usedPercent: 63.5,
257
+ includedTokenCount: 9,
258
+ totalTokenCount: 10,
259
+ });
260
+ } finally {
261
+ fs.rmSync(dataDir, { recursive: true, force: true });
262
+ }
263
+ });
264
+
265
+ it('reads a token-status.json written without the seven day window aggregate', () => {
266
+ const dataDir = makeDataDir();
267
+ try {
268
+ fs.writeFileSync(
269
+ path.join(dataDir, 'token-status.json'),
270
+ JSON.stringify({
271
+ tokens: [
272
+ {
273
+ name: 'alice',
274
+ fiveHourUtilizationPercent: 10,
275
+ fiveHourResetSeconds: 3600,
276
+ sevenDayUtilizationPercent: 12,
277
+ sevenDayResetSeconds: 432000,
278
+ color: 'G',
279
+ prep: 2,
280
+ hum: 1,
281
+ },
282
+ ],
283
+ capturedAt: 'x',
284
+ }),
285
+ );
286
+ const input = buildComposeDashboardInput({
287
+ dashboardDataDir: dataDir,
288
+ projectNames: [],
289
+ });
290
+ expect(input.sevenDayWindowAggregate).toBeNull();
291
+ expect(input.tokens).toHaveLength(1);
292
+ } finally {
293
+ fs.rmSync(dataDir, { recursive: true, force: true });
294
+ }
295
+ });
296
+
297
+ it('ignores a seven day window aggregate with a missing field', () => {
298
+ const dataDir = makeDataDir();
299
+ try {
300
+ fs.writeFileSync(
301
+ path.join(dataDir, 'token-status.json'),
302
+ JSON.stringify({
303
+ tokens: [],
304
+ sevenDayWindowAggregate: { usedPercent: 63 },
305
+ capturedAt: 'x',
306
+ }),
307
+ );
308
+ const input = buildComposeDashboardInput({
309
+ dashboardDataDir: dataDir,
310
+ projectNames: [],
311
+ });
312
+ expect(input.sevenDayWindowAggregate).toBeNull();
313
+ } finally {
314
+ fs.rmSync(dataDir, { recursive: true, force: true });
315
+ }
316
+ });
317
+
236
318
  it('defaults an unknown token color to Y and null windows to null', () => {
237
319
  const dataDir = makeDataDir();
238
320
  try {
@@ -10,6 +10,7 @@ import {
10
10
  import { toDashboardDisplayLabel } from '../../../domain/usecases/dashboard/DashboardProjectCode';
11
11
  import { DashboardRow } from '../../../domain/usecases/dashboard/GenerateDashboardRowUseCase';
12
12
  import {
13
+ SevenDayWindowAggregate,
13
14
  TokenStatus,
14
15
  TokenStatusColor,
15
16
  } from '../../../domain/usecases/dashboard/GenerateTokenStatusUseCase';
@@ -163,10 +164,36 @@ const parseTokenStatus = (value: unknown): TokenStatus | null => {
163
164
  };
164
165
  };
165
166
 
166
- const readTokenStatuses = (dashboardDataDir: string): TokenStatus[] => {
167
+ const parseSevenDayWindowAggregate = (
168
+ value: unknown,
169
+ ): SevenDayWindowAggregate | null => {
170
+ if (!isRecord(value)) {
171
+ return null;
172
+ }
173
+ const usedPercent = asFiniteNumber(value.usedPercent);
174
+ const includedTokenCount = asFiniteNumber(value.includedTokenCount);
175
+ const totalTokenCount = asFiniteNumber(value.totalTokenCount);
176
+ if (
177
+ usedPercent === null ||
178
+ includedTokenCount === null ||
179
+ totalTokenCount === null
180
+ ) {
181
+ return null;
182
+ }
183
+ return { usedPercent, includedTokenCount, totalTokenCount };
184
+ };
185
+
186
+ type TokenStatusFileContent = {
187
+ tokens: TokenStatus[];
188
+ sevenDayWindowAggregate: SevenDayWindowAggregate | null;
189
+ };
190
+
191
+ const readTokenStatusFile = (
192
+ dashboardDataDir: string,
193
+ ): TokenStatusFileContent => {
167
194
  const value = readJsonFile(path.join(dashboardDataDir, 'token-status.json'));
168
195
  if (!isRecord(value) || !Array.isArray(value.tokens)) {
169
- return [];
196
+ return { tokens: [], sevenDayWindowAggregate: null };
170
197
  }
171
198
  const tokens: TokenStatus[] = [];
172
199
  for (const entry of value.tokens) {
@@ -175,7 +202,12 @@ const readTokenStatuses = (dashboardDataDir: string): TokenStatus[] => {
175
202
  tokens.push(token);
176
203
  }
177
204
  }
178
- return tokens;
205
+ return {
206
+ tokens,
207
+ sevenDayWindowAggregate: parseSevenDayWindowAggregate(
208
+ value.sevenDayWindowAggregate,
209
+ ),
210
+ };
179
211
  };
180
212
 
181
213
  export const buildComposeDashboardInput = (
@@ -187,10 +219,12 @@ export const buildComposeDashboardInput = (
187
219
  row: readProjectRow(options.dashboardDataDir, projectName),
188
220
  }),
189
221
  );
222
+ const tokenStatusFile = readTokenStatusFile(options.dashboardDataDir);
190
223
  return {
191
224
  projects,
192
225
  machineStatus: readMachineStatus(options.dashboardDataDir),
193
- tokens: readTokenStatuses(options.dashboardDataDir),
226
+ tokens: tokenStatusFile.tokens,
227
+ sevenDayWindowAggregate: tokenStatusFile.sevenDayWindowAggregate,
194
228
  };
195
229
  };
196
230
 
@@ -128,6 +128,11 @@ describe('writeTokenStatus', () => {
128
128
  const written = readJson(path.join(dir, 'token-status.json'));
129
129
  const expected: TokenStatusFile = {
130
130
  capturedAt: '2026-06-26T12:00:00.000Z',
131
+ sevenDayWindowAggregate: {
132
+ usedPercent: 50,
133
+ includedTokenCount: 1,
134
+ totalTokenCount: 2,
135
+ },
131
136
  tokens: [
132
137
  {
133
138
  name: 'alice',
@@ -216,6 +221,7 @@ describe('writeTokenStatus', () => {
216
221
  };
217
222
  expect(written).toEqual({
218
223
  capturedAt: '2026-06-26T12:00:00.000Z',
224
+ sevenDayWindowAggregate: null,
219
225
  tokens: [
220
226
  expectedAlice,
221
227
  {
@@ -4,9 +4,11 @@ import type { Issue } from '../../../domain/entities/Issue';
4
4
  import { IN_TMUX_STATUS_NAME } from '../../../domain/entities/WorkflowStatus';
5
5
  import {
6
6
  GenerateTokenStatusUseCase,
7
+ SevenDayWindowAggregate,
7
8
  TokenRateLimitSnapshot,
8
9
  TokenStatus,
9
10
  TokenStatusInput,
11
+ computeSevenDayWindowAggregate,
10
12
  } from '../../../domain/usecases/dashboard/GenerateTokenStatusUseCase';
11
13
  import { InTmuxByHumanSessionTokenCountUseCase } from '../../../domain/usecases/InTmuxByHumanSessionTokenCountUseCase';
12
14
  import { OauthTokenCandidate } from '../../../domain/usecases/OauthTokenSelectUseCase';
@@ -35,6 +37,7 @@ export type TokenStatusWriterParams = {
35
37
 
36
38
  export type TokenStatusFile = {
37
39
  tokens: TokenStatus[];
40
+ sevenDayWindowAggregate: SevenDayWindowAggregate | null;
38
41
  capturedAt: string;
39
42
  };
40
43
 
@@ -247,6 +250,7 @@ export const writeTokenStatus = (params: TokenStatusWriterParams): void => {
247
250
 
248
251
  const file: TokenStatusFile = {
249
252
  tokens,
253
+ sevenDayWindowAggregate: computeSevenDayWindowAggregate(tokens),
250
254
  capturedAt: now.toISOString(),
251
255
  };
252
256
 
@@ -1,7 +1,10 @@
1
1
  import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
+ import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
4
5
  import { FileSystemSubAgentLivenessResolver } from './FileSystemSubAgentLivenessResolver';
6
+ import { FileSystemSubAgentTranscriptDirectoryResolver } from './FileSystemSubAgentTranscriptDirectoryResolver';
7
+ import { TranscriptSessionSubAgentActivityRepository } from './TranscriptSessionSubAgentActivityRepository';
5
8
 
6
9
  describe('FileSystemSubAgentLivenessResolver', () => {
7
10
  let runtimeRoot: string;
@@ -40,6 +43,20 @@ describe('FileSystemSubAgentLivenessResolver', () => {
40
43
  expect(result).toEqual(new Set(['a70a08e0587d6f484', 'ad5d15d239d7e72e8']));
41
44
  });
42
45
 
46
+ it('returns only the leading agent id token of each record line', async () => {
47
+ writeRunningFile(
48
+ 'ad3d5702d1339492e impl:redcircle-only-tag-format (no branch)\naf19c02b4c3a5d6e7 chore:board-sync i1300-record-parsing\n',
49
+ );
50
+ const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
51
+
52
+ const result = await resolver.resolveLiveSubAgentIds({
53
+ sessionName,
54
+ mainTranscriptPath,
55
+ });
56
+
57
+ expect(result).toEqual(new Set(['ad3d5702d1339492e', 'af19c02b4c3a5d6e7']));
58
+ });
59
+
43
60
  it('returns an empty set when the running file exists but lists no ids', async () => {
44
61
  writeRunningFile('\n \n');
45
62
  const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
@@ -85,3 +102,80 @@ describe('FileSystemSubAgentLivenessResolver', () => {
85
102
  expect(result).toBeNull();
86
103
  });
87
104
  });
105
+
106
+ describe('running-subagents record agent id contract with TranscriptSessionSubAgentActivityRepository', () => {
107
+ const cwdSlug = '-home-user-worktrees-issue-1300';
108
+ const sessionUuid = 'c0f1a2b3-4d5e-6f70-8192-a3b4c5d6e7f8';
109
+ const sessionName = 'https_//github_com/owner/repo/issues/1300';
110
+ const liveAgentId = 'ad3d5702d1339492e';
111
+ const now = new Date('2026-07-28T12:00:00.000Z');
112
+ const emptyProcessLister: SubAgentProcessLister = {
113
+ listProcesses: async () => [],
114
+ };
115
+ let runtimeRoot: string;
116
+ let transcriptRoot: string;
117
+ let mainTranscriptPath: string;
118
+
119
+ beforeEach(() => {
120
+ runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'subagent-live-rt-'));
121
+ transcriptRoot = fs.mkdtempSync(
122
+ path.join(os.tmpdir(), 'subagent-live-tx-'),
123
+ );
124
+ mainTranscriptPath = path.join(
125
+ transcriptRoot,
126
+ cwdSlug,
127
+ `${sessionUuid}.jsonl`,
128
+ );
129
+ fs.mkdirSync(path.dirname(mainTranscriptPath), { recursive: true });
130
+ fs.writeFileSync(mainTranscriptPath, '', 'utf8');
131
+ });
132
+
133
+ afterEach(() => {
134
+ fs.rmSync(runtimeRoot, { force: true, recursive: true });
135
+ fs.rmSync(transcriptRoot, { force: true, recursive: true });
136
+ });
137
+
138
+ it('keeps a sub-agent whose id is recorded together with its label on one line', async () => {
139
+ const runningDirectory = path.join(runtimeRoot, cwdSlug, sessionUuid);
140
+ fs.mkdirSync(runningDirectory, { recursive: true });
141
+ fs.writeFileSync(
142
+ path.join(runningDirectory, 'running-subagents.txt'),
143
+ `${liveAgentId} impl:subagent-liveness-record-parsing (no branch)\n`,
144
+ 'utf8',
145
+ );
146
+ const subAgentsDirectory = path.join(
147
+ transcriptRoot,
148
+ cwdSlug,
149
+ sessionUuid,
150
+ 'subagents',
151
+ );
152
+ fs.mkdirSync(subAgentsDirectory, { recursive: true });
153
+ fs.writeFileSync(
154
+ path.join(subAgentsDirectory, `agent-${liveAgentId}.jsonl`),
155
+ JSON.stringify({
156
+ type: 'assistant',
157
+ timestamp: '2026-07-28T11:00:00.000Z',
158
+ message: {
159
+ stop_reason: 'tool_use',
160
+ content: [{ type: 'tool_use', name: 'Read', input: {} }],
161
+ },
162
+ }),
163
+ 'utf8',
164
+ );
165
+ const repository = new TranscriptSessionSubAgentActivityRepository(
166
+ new FileSystemSubAgentTranscriptDirectoryResolver(transcriptRoot),
167
+ emptyProcessLister,
168
+ now,
169
+ new FileSystemSubAgentLivenessResolver(runtimeRoot),
170
+ );
171
+
172
+ const result = await repository.listSubAgentActivitiesBySessionName(
173
+ [sessionName],
174
+ new Map([[sessionName, mainTranscriptPath]]),
175
+ );
176
+
177
+ expect(result.get(sessionName)?.map((activity) => activity.label)).toEqual([
178
+ `agent-${liveAgentId}`,
179
+ ]);
180
+ });
181
+ });
@@ -25,9 +25,9 @@ export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResol
25
25
  }
26
26
  const liveIds = new Set<string>();
27
27
  for (const line of content.split('\n')) {
28
- const trimmed = line.trim();
29
- if (trimmed.length > 0) {
30
- liveIds.add(trimmed);
28
+ const agentId = line.trim().split(/\s+/)[0];
29
+ if (agentId.length > 0) {
30
+ liveIds.add(agentId);
31
31
  }
32
32
  }
33
33
  return liveIds;