github-issue-tower-defence-management 1.152.3 → 1.153.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 +7 -0
  2. package/README.md +16 -1
  3. package/bin/adapter/entry-points/cli/index.js +36 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/webServer.js +59 -23
  6. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +11 -0
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js +20 -0
  10. package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js.map +1 -0
  11. package/bin/adapter/entry-points/handlers/ownerCallFileStore.js +80 -0
  12. package/bin/adapter/entry-points/handlers/ownerCallFileStore.js.map +1 -0
  13. package/bin/domain/usecases/intmux/OwnerCallFile.js +52 -0
  14. package/bin/domain/usecases/intmux/OwnerCallFile.js.map +1 -0
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/cli/index.test.ts +189 -0
  17. package/src/adapter/entry-points/cli/index.ts +75 -0
  18. package/src/adapter/entry-points/console/webServer.test.ts +317 -0
  19. package/src/adapter/entry-points/console/webServer.ts +86 -26
  20. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +35 -0
  21. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +15 -0
  22. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +109 -0
  23. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.ts +25 -0
  24. package/src/adapter/entry-points/handlers/ownerCallFileStore.test.ts +286 -0
  25. package/src/adapter/entry-points/handlers/ownerCallFileStore.ts +145 -0
  26. package/src/domain/usecases/intmux/OwnerCallFile.test.ts +184 -0
  27. package/src/domain/usecases/intmux/OwnerCallFile.ts +80 -0
  28. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  29. package/types/adapter/entry-points/console/webServer.d.ts +1 -0
  30. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  31. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  32. package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts +8 -0
  33. package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts.map +1 -0
  34. package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts +21 -0
  35. package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts.map +1 -0
  36. package/types/domain/usecases/intmux/OwnerCallFile.d.ts +18 -0
  37. package/types/domain/usecases/intmux/OwnerCallFile.d.ts.map +1 -0
@@ -3,6 +3,7 @@ import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
5
  import { mock } from 'jest-mock-extended';
6
+ import { parseAllDocuments } from 'yaml';
6
7
  import {
7
8
  DEFAULT_WEB_PORT,
8
9
  CONSOLE_TOKEN_HEADER,
@@ -13,6 +14,7 @@ import {
13
14
  isConsoleAppRoute,
14
15
  extractProvidedToken,
15
16
  extractCookieToken,
17
+ isOwnerCallFileRequestPath,
16
18
  resolveFlatInTmuxFilePath,
17
19
  resolveDashboardFilePath,
18
20
  startWebServer,
@@ -23,6 +25,9 @@ import { readDoneProjectItemIds } from './consoleDoneStore';
23
25
  import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
24
26
  import { Project } from '../../../domain/entities/Project';
25
27
  import { Issue } from '../../../domain/entities/Issue';
28
+ import { ownerCallFileRelativePath } from '../../../domain/usecases/intmux/OwnerCallFile';
29
+ import { toTmuxSessionName } from '../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase';
30
+ import { ownerCallFileAppend } from '../handlers/ownerCallFileStore';
26
31
 
27
32
  describe('webServer pure helpers', () => {
28
33
  describe('DEFAULT_WEB_PORT', () => {
@@ -983,6 +988,46 @@ describe('resolveFlatInTmuxFilePath', () => {
983
988
  resolveFlatInTmuxFilePath(baseDir, '/in-tmux-by-human/..%2fsecret.json'),
984
989
  ).toBeNull();
985
990
  });
991
+
992
+ it('resolves an owner call file that sits one directory deep', () => {
993
+ expect(
994
+ resolveFlatInTmuxFilePath(
995
+ baseDir,
996
+ '/in-tmux-by-human/call-to-user/umino/secretary.yaml',
997
+ ),
998
+ ).toBe(
999
+ path.join(
1000
+ path.resolve(baseDir),
1001
+ 'call-to-user',
1002
+ 'umino',
1003
+ 'secretary.yaml',
1004
+ ),
1005
+ );
1006
+ });
1007
+
1008
+ it('returns null for a yaml file outside the owner call directory shape', () => {
1009
+ expect(
1010
+ resolveFlatInTmuxFilePath(baseDir, '/in-tmux-by-human/secretary.yaml'),
1011
+ ).toBeNull();
1012
+ expect(
1013
+ resolveFlatInTmuxFilePath(
1014
+ baseDir,
1015
+ '/in-tmux-by-human/other-directory/umino/secretary.yaml',
1016
+ ),
1017
+ ).toBeNull();
1018
+ expect(
1019
+ resolveFlatInTmuxFilePath(
1020
+ baseDir,
1021
+ '/in-tmux-by-human/call-to-user/umino/nested/secretary.yaml',
1022
+ ),
1023
+ ).toBeNull();
1024
+ expect(
1025
+ resolveFlatInTmuxFilePath(
1026
+ baseDir,
1027
+ '/in-tmux-by-human/call-to-user/umino/secretary.json',
1028
+ ),
1029
+ ).toBeNull();
1030
+ });
986
1031
  });
987
1032
 
988
1033
  describe('resolveDashboardFilePath', () => {
@@ -1945,3 +1990,275 @@ describe('webServer token cookie redirect', () => {
1945
1990
  }
1946
1991
  });
1947
1992
  });
1993
+
1994
+ const toPlainValue = (document: { toJS: () => unknown }): unknown =>
1995
+ document.toJS();
1996
+
1997
+ describe('webServer owner call file route', () => {
1998
+ const testToken = 'owner-call-token-value';
1999
+ const sessionName = toTmuxSessionName(
2000
+ 'https://github.com/OWNER/REPO/issues/1',
2001
+ );
2002
+ const ownerCall = {
2003
+ sessionName,
2004
+ calledAt: '2026-08-14T04:22:28Z',
2005
+ body: ' the first line of the call body.\n\na later line of the call body.\n',
2006
+ };
2007
+
2008
+ const requestServer = (
2009
+ server: http.Server,
2010
+ requestPath: string,
2011
+ method = 'GET',
2012
+ ): Promise<{
2013
+ statusCode: number;
2014
+ body: string;
2015
+ contentType: string | undefined;
2016
+ }> => {
2017
+ const address = server.address();
2018
+ if (address === null || typeof address === 'string') {
2019
+ throw new Error('server is not listening on a TCP port');
2020
+ }
2021
+ return new Promise((resolve, reject) => {
2022
+ const httpRequest = http.request(
2023
+ { host: '127.0.0.1', port: address.port, path: requestPath, method },
2024
+ (response) => {
2025
+ const chunks: Uint8Array[] = [];
2026
+ response.on('data', (chunk: Uint8Array) => chunks.push(chunk));
2027
+ response.on('end', () => {
2028
+ resolve({
2029
+ statusCode: response.statusCode ?? 0,
2030
+ body: Buffer.concat(chunks).toString('utf-8'),
2031
+ contentType: response.headers['content-type'],
2032
+ });
2033
+ });
2034
+ },
2035
+ );
2036
+ httpRequest.on('error', reject);
2037
+ httpRequest.end();
2038
+ });
2039
+ };
2040
+
2041
+ const closeServer = (server: http.Server): Promise<void> =>
2042
+ new Promise((resolve, reject) => {
2043
+ server.close((error) => {
2044
+ if (error) {
2045
+ reject(error);
2046
+ return;
2047
+ }
2048
+ resolve();
2049
+ });
2050
+ });
2051
+
2052
+ const startWithOwnerCallFile = async (
2053
+ projectCode: string | null,
2054
+ ): Promise<{
2055
+ server: http.Server;
2056
+ tmpDir: string;
2057
+ inTmuxDataDir: string;
2058
+ requestPath: string;
2059
+ }> => {
2060
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owner-call-server-'));
2061
+ const inTmuxDataDir = path.join(tmpDir, 'in-tmux-by-human');
2062
+ fs.mkdirSync(inTmuxDataDir, { recursive: true });
2063
+ fs.writeFileSync(path.join(tmpDir, 'secret.yaml'), 'secret: true\n');
2064
+ ownerCallFileAppend({ dataDir: inTmuxDataDir, projectCode, ownerCall });
2065
+ const server = await startWebServer({
2066
+ accessToken: testToken,
2067
+ uiDistDir: path.join(tmpDir, 'ui-dist'),
2068
+ consoleDataOutputDir: null,
2069
+ inTmuxDataDir,
2070
+ dashboardDir: null,
2071
+ dashboardDataDir: null,
2072
+ dashboardProjectNames: [],
2073
+ port: 0,
2074
+ });
2075
+ return {
2076
+ server,
2077
+ tmpDir,
2078
+ inTmuxDataDir,
2079
+ requestPath: `/in-tmux-by-human/${ownerCallFileRelativePath(
2080
+ projectCode,
2081
+ sessionName,
2082
+ )}`,
2083
+ };
2084
+ };
2085
+
2086
+ it('requires a token for the owner call file path', () => {
2087
+ expect(
2088
+ requiresToken(
2089
+ `/in-tmux-by-human/${ownerCallFileRelativePath('umino', sessionName)}`,
2090
+ ),
2091
+ ).toBe(true);
2092
+ });
2093
+
2094
+ it('recognizes only the owner call file path as deletable', () => {
2095
+ expect(
2096
+ isOwnerCallFileRequestPath(
2097
+ `/in-tmux-by-human/${ownerCallFileRelativePath('umino', sessionName)}`,
2098
+ ),
2099
+ ).toBe(true);
2100
+ expect(isOwnerCallFileRequestPath('/in-tmux-by-human/index.v4.json')).toBe(
2101
+ false,
2102
+ );
2103
+ expect(
2104
+ isOwnerCallFileRequestPath('/in-tmux-by-human/call-to-user/umino.yaml'),
2105
+ ).toBe(false);
2106
+ });
2107
+
2108
+ it('resolves the owner call file the append entry point writes, without a hardcoded path', () => {
2109
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owner-call-path-'));
2110
+ try {
2111
+ ownerCallFileAppend({
2112
+ dataDir,
2113
+ projectCode: 'umino',
2114
+ ownerCall,
2115
+ });
2116
+ const relativePath = ownerCallFileRelativePath('umino', sessionName);
2117
+
2118
+ const resolved = resolveFlatInTmuxFilePath(
2119
+ dataDir,
2120
+ `/in-tmux-by-human/${relativePath}`,
2121
+ );
2122
+
2123
+ expect(resolved).not.toBeNull();
2124
+ expect(fs.readFileSync(String(resolved), 'utf-8')).toBe(
2125
+ fs.readFileSync(path.join(dataDir, relativePath), 'utf-8'),
2126
+ );
2127
+ } finally {
2128
+ fs.rmSync(dataDir, { recursive: true, force: true });
2129
+ }
2130
+ });
2131
+
2132
+ it('serves, then deletes, the file the append entry point wrote', async () => {
2133
+ const { server, tmpDir, requestPath } =
2134
+ await startWithOwnerCallFile('umino');
2135
+ try {
2136
+ const fetched = await requestServer(
2137
+ server,
2138
+ `${requestPath}?k=${testToken}`,
2139
+ );
2140
+ expect(fetched.statusCode).toBe(200);
2141
+ expect(fetched.contentType).toContain('text/yaml');
2142
+ const documents = parseAllDocuments(fetched.body);
2143
+ expect(documents).toHaveLength(1);
2144
+ expect(documents[0].toJS()).toEqual(ownerCall);
2145
+
2146
+ const deleted = await requestServer(
2147
+ server,
2148
+ `${requestPath}?k=${testToken}`,
2149
+ 'DELETE',
2150
+ );
2151
+ expect(deleted.statusCode).toBe(204);
2152
+
2153
+ const refetched = await requestServer(
2154
+ server,
2155
+ `${requestPath}?k=${testToken}`,
2156
+ );
2157
+ expect(refetched.statusCode).toBe(404);
2158
+
2159
+ const deletedAgain = await requestServer(
2160
+ server,
2161
+ `${requestPath}?k=${testToken}`,
2162
+ 'DELETE',
2163
+ );
2164
+ expect(deletedAgain.statusCode).toBe(204);
2165
+ } finally {
2166
+ await closeServer(server);
2167
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2168
+ }
2169
+ });
2170
+
2171
+ it('serves two appended documents oldest first', async () => {
2172
+ const { server, tmpDir, inTmuxDataDir, requestPath } =
2173
+ await startWithOwnerCallFile(null);
2174
+ try {
2175
+ ownerCallFileAppend({
2176
+ dataDir: inTmuxDataDir,
2177
+ projectCode: null,
2178
+ ownerCall: {
2179
+ sessionName,
2180
+ calledAt: '2026-08-14T05:00:00Z',
2181
+ body: 'the newer call\n',
2182
+ },
2183
+ });
2184
+
2185
+ const fetched = await requestServer(
2186
+ server,
2187
+ `${requestPath}?k=${testToken}`,
2188
+ );
2189
+ expect(fetched.statusCode).toBe(200);
2190
+ expect(parseAllDocuments(fetched.body).map(toPlainValue)).toEqual([
2191
+ ownerCall,
2192
+ {
2193
+ sessionName,
2194
+ calledAt: '2026-08-14T05:00:00Z',
2195
+ body: 'the newer call\n',
2196
+ },
2197
+ ]);
2198
+ } finally {
2199
+ await closeServer(server);
2200
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2201
+ }
2202
+ });
2203
+
2204
+ it('rejects a request that carries no access token', async () => {
2205
+ const { server, tmpDir, requestPath } =
2206
+ await startWithOwnerCallFile('umino');
2207
+ try {
2208
+ expect((await requestServer(server, requestPath)).statusCode).toBe(401);
2209
+ expect(
2210
+ (await requestServer(server, requestPath, 'DELETE')).statusCode,
2211
+ ).toBe(401);
2212
+ } finally {
2213
+ await closeServer(server);
2214
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2215
+ }
2216
+ });
2217
+
2218
+ it('rejects a request whose path escapes the data directory and leaves the outside file untouched', async () => {
2219
+ const { server, tmpDir } = await startWithOwnerCallFile('umino');
2220
+ const escapingPath = '/in-tmux-by-human/call-to-user/../../secret.yaml';
2221
+ try {
2222
+ const fetched = await requestServer(
2223
+ server,
2224
+ `${escapingPath}?k=${testToken}`,
2225
+ );
2226
+ expect(fetched.statusCode).toBe(404);
2227
+ expect(fetched.body).not.toContain('secret: true');
2228
+
2229
+ const deleted = await requestServer(
2230
+ server,
2231
+ `${escapingPath}?k=${testToken}`,
2232
+ 'DELETE',
2233
+ );
2234
+ expect(deleted.statusCode).toBe(404);
2235
+ expect(fs.existsSync(path.join(tmpDir, 'secret.yaml'))).toBe(true);
2236
+ } finally {
2237
+ await closeServer(server);
2238
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2239
+ }
2240
+ });
2241
+
2242
+ it('refuses to delete a flat in-tmux json file through the owner call route', async () => {
2243
+ const { server, tmpDir, inTmuxDataDir } =
2244
+ await startWithOwnerCallFile('umino');
2245
+ fs.writeFileSync(
2246
+ path.join(inTmuxDataDir, 'index.v4.json'),
2247
+ '{"version":4,"projects":[]}\n',
2248
+ );
2249
+ try {
2250
+ const deleted = await requestServer(
2251
+ server,
2252
+ `/in-tmux-by-human/index.v4.json?k=${testToken}`,
2253
+ 'DELETE',
2254
+ );
2255
+ expect(deleted.statusCode).toBe(404);
2256
+ expect(fs.existsSync(path.join(inTmuxDataDir, 'index.v4.json'))).toBe(
2257
+ true,
2258
+ );
2259
+ } finally {
2260
+ await closeServer(server);
2261
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2262
+ }
2263
+ });
2264
+ });
@@ -36,6 +36,10 @@ import {
36
36
  composeDashboardText,
37
37
  dashboardComposeFilesPresent,
38
38
  } from './dashboardComposeService';
39
+ import {
40
+ OWNER_CALL_FILE_DIRECTORY_NAME,
41
+ OWNER_CALL_FILE_EXTENSION,
42
+ } from '../../../domain/usecases/intmux/OwnerCallFile';
39
43
 
40
44
  export const DEFAULT_WEB_PORT = 9980;
41
45
 
@@ -74,6 +78,7 @@ const MIME_TYPES: Record<string, string> = {
74
78
  '.map': 'application/json; charset=utf-8',
75
79
  '.woff': 'font/woff',
76
80
  '.woff2': 'font/woff2',
81
+ '.yaml': 'text/yaml; charset=utf-8',
77
82
  };
78
83
 
79
84
  export const hasDotSegment = (requestPath: string): boolean =>
@@ -84,7 +89,8 @@ export const hasDotSegment = (requestPath: string): boolean =>
84
89
  export const requiresToken = (requestPath: string): boolean =>
85
90
  requestPath.startsWith('/api/') ||
86
91
  requestPath === '/api' ||
87
- requestPath.endsWith('.json');
92
+ requestPath.endsWith('.json') ||
93
+ requestPath.endsWith(OWNER_CALL_FILE_EXTENSION);
88
94
 
89
95
  const SAFE_PJCODE = /^[A-Za-z0-9._-]+$/;
90
96
 
@@ -227,6 +233,12 @@ const FLAT_IN_TMUX_PREFIX = '/in-tmux-by-human/';
227
233
 
228
234
  const FLAT_IN_TMUX_FILE = /^[A-Za-z0-9._-]+\.json$/;
229
235
 
236
+ const NAME_SEGMENT = '[A-Za-z0-9_-][A-Za-z0-9._-]*';
237
+
238
+ const OWNER_CALL_FILE = new RegExp(
239
+ `^${OWNER_CALL_FILE_DIRECTORY_NAME}/${NAME_SEGMENT}/${NAME_SEGMENT}${OWNER_CALL_FILE_EXTENSION.replace('.', '\\.')}$`,
240
+ );
241
+
230
242
  export const DASHBOARD_REQUEST_PATH = '/tdpm.txt';
231
243
 
232
244
  export const IMAGE_PROXY_REQUEST_PATH = '/api/img';
@@ -249,6 +261,10 @@ export const resolveDashboardFilePath = (
249
261
  return resolvedCandidate;
250
262
  };
251
263
 
264
+ export const isOwnerCallFileRequestPath = (requestPath: string): boolean =>
265
+ requestPath.startsWith(FLAT_IN_TMUX_PREFIX) &&
266
+ OWNER_CALL_FILE.test(requestPath.slice(FLAT_IN_TMUX_PREFIX.length));
267
+
252
268
  export const resolveFlatInTmuxFilePath = (
253
269
  inTmuxDataDir: string,
254
270
  requestPath: string,
@@ -256,11 +272,16 @@ export const resolveFlatInTmuxFilePath = (
256
272
  if (!requestPath.startsWith(FLAT_IN_TMUX_PREFIX)) {
257
273
  return null;
258
274
  }
259
- const fileName = requestPath.slice(FLAT_IN_TMUX_PREFIX.length);
260
- if (fileName.length === 0 || !FLAT_IN_TMUX_FILE.test(fileName)) {
275
+ const relativePath = requestPath.slice(FLAT_IN_TMUX_PREFIX.length);
276
+ if (
277
+ relativePath.length === 0 ||
278
+ !(
279
+ FLAT_IN_TMUX_FILE.test(relativePath) || OWNER_CALL_FILE.test(relativePath)
280
+ )
281
+ ) {
261
282
  return null;
262
283
  }
263
- const candidate = path.join(inTmuxDataDir, fileName);
284
+ const candidate = path.join(inTmuxDataDir, relativePath);
264
285
  const resolvedRoot = path.resolve(inTmuxDataDir);
265
286
  const resolvedCandidate = path.resolve(candidate);
266
287
  if (!resolvedCandidate.startsWith(resolvedRoot + path.sep)) {
@@ -530,6 +551,56 @@ const handleOperationApi = async (
530
551
  }
531
552
  };
532
553
 
554
+ const serveFlatInTmuxFile = (
555
+ options: WebServerOptions,
556
+ response: http.ServerResponse,
557
+ requestPath: string,
558
+ ): void => {
559
+ if (options.inTmuxDataDir === null) {
560
+ sendNotFound(response);
561
+ return;
562
+ }
563
+ const filePath = resolveFlatInTmuxFilePath(
564
+ options.inTmuxDataDir,
565
+ requestPath,
566
+ );
567
+ const content = filePath === null ? null : readStaticFile(filePath);
568
+ if (filePath === null || content === null) {
569
+ sendNotFound(response);
570
+ return;
571
+ }
572
+ response.writeHead(200, {
573
+ 'Content-Type': contentTypeForPath(filePath),
574
+ 'Cache-Control': 'no-store',
575
+ 'Content-Length': String(content.length),
576
+ });
577
+ response.end(content);
578
+ };
579
+
580
+ const removeOwnerCallFile = (
581
+ options: WebServerOptions,
582
+ response: http.ServerResponse,
583
+ requestPath: string,
584
+ ): void => {
585
+ if (options.inTmuxDataDir === null) {
586
+ sendNotFound(response);
587
+ return;
588
+ }
589
+ const filePath = resolveFlatInTmuxFilePath(
590
+ options.inTmuxDataDir,
591
+ requestPath,
592
+ );
593
+ if (filePath === null) {
594
+ sendNotFound(response);
595
+ return;
596
+ }
597
+ fs.rmSync(filePath, { force: true });
598
+ response.writeHead(204, {
599
+ 'Cache-Control': 'no-store',
600
+ });
601
+ response.end();
602
+ };
603
+
533
604
  const handleTokenedRequest = async (
534
605
  options: WebServerOptions,
535
606
  request: http.IncomingMessage,
@@ -580,31 +651,20 @@ const handleTokenedRequest = async (
580
651
  return;
581
652
  }
582
653
 
583
- if (method === 'GET') {
584
- if (requestPath.startsWith(FLAT_IN_TMUX_PREFIX)) {
585
- if (options.inTmuxDataDir === null) {
586
- sendNotFound(response);
587
- return;
588
- }
589
- const flatFilePath = resolveFlatInTmuxFilePath(
590
- options.inTmuxDataDir,
591
- requestPath,
592
- );
593
- const flatContent =
594
- flatFilePath === null ? null : readStaticFile(flatFilePath);
595
- if (flatContent === null) {
596
- sendNotFound(response);
597
- return;
598
- }
599
- response.writeHead(200, {
600
- 'Content-Type': 'application/json; charset=utf-8',
601
- 'Cache-Control': 'no-store',
602
- 'Content-Length': String(flatContent.length),
603
- });
604
- response.end(flatContent);
654
+ if (requestPath.startsWith(FLAT_IN_TMUX_PREFIX)) {
655
+ if (method === 'GET') {
656
+ serveFlatInTmuxFile(options, response, requestPath);
605
657
  return;
606
658
  }
659
+ if (method === 'DELETE' && isOwnerCallFileRequestPath(requestPath)) {
660
+ removeOwnerCallFile(options, response, requestPath);
661
+ return;
662
+ }
663
+ sendNotFound(response);
664
+ return;
665
+ }
607
666
 
667
+ if (method === 'GET') {
608
668
  const dataRoute = parseConsoleDataRoute(requestPath);
609
669
  if (dataRoute !== null && options.consoleDataOutputDir !== null) {
610
670
  const dataResponse = buildConsoleDataResponse(
@@ -129,6 +129,9 @@ jest.mock('./rotationOrderFileWriter', () => ({
129
129
  jest.mock('./inTmuxByHumanDataWriter', () => ({
130
130
  writeInTmuxByHumanData: jest.fn(),
131
131
  }));
132
+ jest.mock('./ownerCallFileCleaner', () => ({
133
+ cleanClosedIssueOwnerCallFiles: jest.fn(),
134
+ }));
132
135
  jest.mock('./consoleListsWriter', () => {
133
136
  const actual = jest.requireActual<typeof import('./consoleListsWriter')>(
134
137
  './consoleListsWriter',
@@ -139,6 +142,7 @@ jest.mock('./consoleListsWriter', () => {
139
142
  import { HandleScheduledEventUseCaseHandler } from './HandleScheduledEventUseCaseHandler';
140
143
  import { writeSituationFile } from './situationFileWriter';
141
144
  import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
145
+ import { cleanClosedIssueOwnerCallFiles } from './ownerCallFileCleaner';
142
146
  import { writeConsoleLists } from './consoleListsWriter';
143
147
  import { GraphqlProjectRepository } from '../../repositories/GraphqlProjectRepository';
144
148
  import { ApiV3IssueRepository } from '../../repositories/issue/ApiV3IssueRepository';
@@ -756,4 +760,35 @@ defaultAgentName: readme-agent
756
760
  expect(getInTmuxProjectOrderArg()).toBeNull();
757
761
  });
758
762
  });
763
+
764
+ describe('owner call file cleanup of closed issues', () => {
765
+ it('should hand the project issues to the cleanup with the configured project code and data directory', async () => {
766
+ jest.mocked(fs.readFileSync).mockReturnValue(
767
+ YAML.stringify({
768
+ ...validConfig,
769
+ inTmuxDataOutputDir: '/data/in-tmux-by-human',
770
+ }),
771
+ );
772
+ const closedIssue = {
773
+ url: 'https://github.com/TestOrg/test-repo/issues/9',
774
+ isClosed: true,
775
+ };
776
+ mockRun.mockResolvedValueOnce({
777
+ project: { id: 'PVT_kwHOtest123' },
778
+ issues: [closedIssue],
779
+ cacheUsed: false,
780
+ targetDateTimes: [],
781
+ rotationOrder: null,
782
+ });
783
+
784
+ const handler = new HandleScheduledEventUseCaseHandler();
785
+ await handler.handle('config.yml', false);
786
+
787
+ expect(jest.mocked(cleanClosedIssueOwnerCallFiles)).toHaveBeenCalledWith({
788
+ inTmuxDataOutputDir: '/data/in-tmux-by-human',
789
+ pjcode: validConfig.projectName,
790
+ issues: [closedIssue],
791
+ });
792
+ });
793
+ });
759
794
  });
@@ -11,6 +11,7 @@ import { writeDashboardRow } from './dashboardRowWriter';
11
11
  import { writeMachineStatus } from './machineStatusWriter';
12
12
  import { writeTokenStatus } from './tokenStatusWriter';
13
13
  import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
14
+ import { cleanClosedIssueOwnerCallFiles } from './ownerCallFileCleaner';
14
15
  import { reconcileInTmuxByHumanSessions } from './inTmuxByHumanSessionReconciler';
15
16
  import { handleTokenExhaustionHandover } from './tokenExhaustionHandover';
16
17
  import { cleanStaleTmuxSessions } from './staleTmuxSessionCleaner';
@@ -682,6 +683,20 @@ export class HandleScheduledEventUseCaseHandler {
682
683
  );
683
684
  }
684
685
 
686
+ try {
687
+ cleanClosedIssueOwnerCallFiles({
688
+ inTmuxDataOutputDir: mergedInput.inTmuxDataOutputDir ?? null,
689
+ pjcode: input.projectName,
690
+ issues: result.issues,
691
+ });
692
+ } catch (error) {
693
+ console.error(
694
+ `Failed to clean owner call files of closed issues: ${
695
+ error instanceof Error ? error.message : String(error)
696
+ }`,
697
+ );
698
+ }
699
+
685
700
  try {
686
701
  await handleTokenExhaustionHandover({
687
702
  enabled: mergedInput.tokenExhaustionHandoverEnabled ?? false,