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.
- package/CHANGELOG.md +13 -0
- package/README.md +6 -6
- package/bin/adapter/entry-points/console/dashboardComposeService.js +23 -4
- package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -1
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js +3 -3
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js.map +1 -1
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +23 -9
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
- package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js +41 -5
- package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js.map +1 -1
- package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js +16 -1
- package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/dashboardComposeService.test.ts +82 -0
- package/src/adapter/entry-points/console/dashboardComposeService.ts +38 -4
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +6 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +4 -0
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.test.ts +94 -0
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +3 -3
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +368 -1
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +28 -10
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +31 -0
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +164 -0
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.ts +63 -5
- package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.test.ts +73 -0
- package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.ts +23 -0
- package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +2 -1
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +2 -1
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts +3 -1
- package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts +6 -0
- 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
|
|
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
|
|
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:
|
|
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
|
|
29
|
-
if (
|
|
30
|
-
liveIds.add(
|
|
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;
|