github-issue-tower-defence-management 1.173.2 → 1.174.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 +19 -0
- package/README.md +15 -0
- package/bin/adapter/entry-points/cli/index.js +5 -3
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js +16 -5
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-BcnLfodS.js → index-g4SqIzCh.js} +2 -2
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +16 -2
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/LocalCommandIssueAttachmentRepository.js +7 -12
- package/bin/adapter/repositories/LocalCommandIssueAttachmentRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +26 -3
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +10 -5
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +142 -0
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -0
- package/bin/domain/usecases/agentReportPrefix.js +5 -0
- package/bin/domain/usecases/agentReportPrefix.js.map +1 -0
- package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js +2 -2
- package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js.map +1 -1
- package/bin/domain/usecases/isRecord.js +6 -0
- package/bin/domain/usecases/isRecord.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +8 -4
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.test.ts +29 -2
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.ts +21 -4
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +5 -1
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.test.ts +8 -5
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.ts +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +2 -2
- package/src/adapter/entry-points/console/ui-dist/assets/{index-BcnLfodS.js → index-g4SqIzCh.js} +2 -2
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +56 -5
- package/src/adapter/entry-points/console/webServer.ts +20 -3
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -0
- package/src/adapter/repositories/LocalCommandIssueAttachmentRepository.test.ts +53 -70
- package/src/adapter/repositories/LocalCommandIssueAttachmentRepository.ts +10 -13
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +142 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +41 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +27 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +7 -3
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +879 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +247 -0
- package/src/domain/usecases/agentReportPrefix.ts +1 -0
- package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +1 -1
- package/src/domain/usecases/isRecord.ts +2 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/LocalCommandIssueAttachmentRepository.d.ts +3 -2
- package/types/adapter/repositories/LocalCommandIssueAttachmentRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +3 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts +15 -0
- package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -0
- package/types/domain/usecases/agentReportPrefix.d.ts +2 -0
- package/types/domain/usecases/agentReportPrefix.d.ts.map +1 -0
- package/types/domain/usecases/isRecord.d.ts +2 -0
- package/types/domain/usecases/isRecord.d.ts.map +1 -0
|
@@ -69,6 +69,7 @@ import { ProxyRateLimitCacheRepository } from '../../repositories/ProxyRateLimit
|
|
|
69
69
|
import { UpdateRateLimitCacheUseCase } from '../../../domain/usecases/UpdateRateLimitCacheUseCase';
|
|
70
70
|
import { RevertOrphanedPreparationUseCase } from '../../../domain/usecases/RevertOrphanedPreparationUseCase';
|
|
71
71
|
import { RevertNotReadyReviewQueueIssueUseCase } from '../../../domain/usecases/RevertNotReadyReviewQueueIssueUseCase';
|
|
72
|
+
import { TriagerApprovalDispatchUseCase } from '../../../domain/usecases/TriagerApprovalDispatchUseCase';
|
|
72
73
|
import { AgentDesignationLabelAdoptUseCase } from '../../../domain/usecases/AgentDesignationLabelAdoptUseCase';
|
|
73
74
|
import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueCommentRepository';
|
|
74
75
|
import { ProjectRequiredFieldCreateUseCase } from '../../../domain/usecases/ProjectRequiredFieldCreateUseCase';
|
|
@@ -496,6 +497,11 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
496
497
|
issueRepository,
|
|
497
498
|
issueCommentRepository,
|
|
498
499
|
);
|
|
500
|
+
const triagerApprovalDispatchUseCase = new TriagerApprovalDispatchUseCase(
|
|
501
|
+
projectRepository,
|
|
502
|
+
issueRepository,
|
|
503
|
+
issueCommentRepository,
|
|
504
|
+
);
|
|
499
505
|
const agentDesignationLabelAdoptUseCase =
|
|
500
506
|
new AgentDesignationLabelAdoptUseCase(projectRepository, issueRepository);
|
|
501
507
|
|
|
@@ -533,6 +539,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
533
539
|
startPreparationUseCase,
|
|
534
540
|
revertOrphanedPreparationUseCase,
|
|
535
541
|
revertNotReadyReviewQueueIssueUseCase,
|
|
542
|
+
triagerApprovalDispatchUseCase,
|
|
536
543
|
agentDesignationLabelAdoptUseCase,
|
|
537
544
|
updateRateLimitCacheUseCase,
|
|
538
545
|
dailySecurityScanUseCase,
|
|
@@ -79,6 +79,7 @@ const createSubAgentActivityRepository = (
|
|
|
79
79
|
new NodeSubAgentProcessLister(localCommandRunner),
|
|
80
80
|
now,
|
|
81
81
|
new FileSystemSubAgentLivenessResolver(subAgentRuntimeRootDirectory),
|
|
82
|
+
subAgentRuntimeRootDirectory,
|
|
82
83
|
);
|
|
83
84
|
}
|
|
84
85
|
return new ProcessListSessionSubAgentActivityRepository(
|
|
@@ -66,6 +66,44 @@ describe('relabelAttachmentMarkdown', () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
describe('LocalCommandIssueAttachmentRepository', () => {
|
|
69
|
+
const resolveGithubToken = (): string => 'default-token';
|
|
70
|
+
|
|
71
|
+
it('should run the upload command with the github token resolved for the item url', async () => {
|
|
72
|
+
const root = await mkdtemp(join(tmpdir(), 'attachment-test-root-'));
|
|
73
|
+
const received: {
|
|
74
|
+
args: string[];
|
|
75
|
+
options: LocalCommandRunnerOptions | undefined;
|
|
76
|
+
}[] = [];
|
|
77
|
+
const runner: LocalCommandRunner = {
|
|
78
|
+
runCommand: async (_program, args, options) => {
|
|
79
|
+
received.push({ args, options });
|
|
80
|
+
return {
|
|
81
|
+
stdout:
|
|
82
|
+
'\n',
|
|
83
|
+
stderr: '',
|
|
84
|
+
exitCode: 0,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const repository = new LocalCommandIssueAttachmentRepository(
|
|
89
|
+
runner,
|
|
90
|
+
(issueOrPullRequestUrl) =>
|
|
91
|
+
issueOrPullRequestUrl.startsWith('https://github.com/acme/')
|
|
92
|
+
? 'acme-token'
|
|
93
|
+
: 'other-token',
|
|
94
|
+
root,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await repository.uploadAttachment({
|
|
98
|
+
issueOrPullRequestUrl: 'https://github.com/acme/repo/issues/7',
|
|
99
|
+
fileName: 'screenshot.png',
|
|
100
|
+
content: new Uint8Array([1]),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(received).toHaveLength(1);
|
|
104
|
+
expect(received[0].options?.env).toEqual({ GH_TOKEN: 'acme-token' });
|
|
105
|
+
});
|
|
106
|
+
|
|
69
107
|
it('should upload the written file and return the markdown the command printed', async () => {
|
|
70
108
|
const root = await mkdtemp(join(tmpdir(), 'attachment-test-root-'));
|
|
71
109
|
const received: { program: string; args: string[] }[] = [];
|
|
@@ -80,7 +118,11 @@ describe('LocalCommandIssueAttachmentRepository', () => {
|
|
|
80
118
|
};
|
|
81
119
|
},
|
|
82
120
|
};
|
|
83
|
-
const repository = new LocalCommandIssueAttachmentRepository(
|
|
121
|
+
const repository = new LocalCommandIssueAttachmentRepository(
|
|
122
|
+
runner,
|
|
123
|
+
resolveGithubToken,
|
|
124
|
+
root,
|
|
125
|
+
);
|
|
84
126
|
|
|
85
127
|
const markdown = await repository.uploadAttachment({
|
|
86
128
|
issueOrPullRequestUrl: 'https://github.com/owner/repo/issues/1',
|
|
@@ -107,7 +149,11 @@ describe('LocalCommandIssueAttachmentRepository', () => {
|
|
|
107
149
|
exitCode: 1,
|
|
108
150
|
}),
|
|
109
151
|
};
|
|
110
|
-
const repository = new LocalCommandIssueAttachmentRepository(
|
|
152
|
+
const repository = new LocalCommandIssueAttachmentRepository(
|
|
153
|
+
runner,
|
|
154
|
+
resolveGithubToken,
|
|
155
|
+
root,
|
|
156
|
+
);
|
|
111
157
|
|
|
112
158
|
await expect(
|
|
113
159
|
repository.uploadAttachment({
|
|
@@ -126,7 +172,11 @@ describe('LocalCommandIssueAttachmentRepository', () => {
|
|
|
126
172
|
const runner: LocalCommandRunner = {
|
|
127
173
|
runCommand: async () => ({ stdout: '\n', stderr: '', exitCode: 0 }),
|
|
128
174
|
};
|
|
129
|
-
const repository = new LocalCommandIssueAttachmentRepository(
|
|
175
|
+
const repository = new LocalCommandIssueAttachmentRepository(
|
|
176
|
+
runner,
|
|
177
|
+
resolveGithubToken,
|
|
178
|
+
root,
|
|
179
|
+
);
|
|
130
180
|
|
|
131
181
|
await expect(
|
|
132
182
|
repository.uploadAttachment({
|
|
@@ -136,71 +186,4 @@ describe('LocalCommandIssueAttachmentRepository', () => {
|
|
|
136
186
|
}),
|
|
137
187
|
).rejects.toThrow(`${UPLOAD_COMMAND} returned no markdown`);
|
|
138
188
|
});
|
|
139
|
-
|
|
140
|
-
it('should pass GH_TOKEN env resolved from the owner when resolveGithubToken is provided', async () => {
|
|
141
|
-
const root = await mkdtemp(join(tmpdir(), 'attachment-test-root-'));
|
|
142
|
-
const received: {
|
|
143
|
-
program: string;
|
|
144
|
-
args: string[];
|
|
145
|
-
options: LocalCommandRunnerOptions | undefined;
|
|
146
|
-
}[] = [];
|
|
147
|
-
const runner: LocalCommandRunner = {
|
|
148
|
-
runCommand: async (program, args, options) => {
|
|
149
|
-
received.push({ program, args, options });
|
|
150
|
-
return {
|
|
151
|
-
stdout:
|
|
152
|
-
'\n',
|
|
153
|
-
stderr: '',
|
|
154
|
-
exitCode: 0,
|
|
155
|
-
};
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
const resolveGithubToken = (owner: string): string => `token-for-${owner}`;
|
|
159
|
-
const repository = new LocalCommandIssueAttachmentRepository(
|
|
160
|
-
runner,
|
|
161
|
-
root,
|
|
162
|
-
resolveGithubToken,
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
await repository.uploadAttachment({
|
|
166
|
-
issueOrPullRequestUrl: 'https://github.com/meta-site/repo/issues/1',
|
|
167
|
-
fileName: 'screenshot.png',
|
|
168
|
-
content: new Uint8Array([1, 2, 3]),
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
expect(received).toHaveLength(1);
|
|
172
|
-
expect(received[0].options).toEqual({
|
|
173
|
-
env: { GH_TOKEN: 'token-for-meta-site' },
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should pass undefined as third argument to runCommand when resolveGithubToken is not provided', async () => {
|
|
178
|
-
const root = await mkdtemp(join(tmpdir(), 'attachment-test-root-'));
|
|
179
|
-
const received: {
|
|
180
|
-
program: string;
|
|
181
|
-
args: string[];
|
|
182
|
-
options: LocalCommandRunnerOptions | undefined;
|
|
183
|
-
}[] = [];
|
|
184
|
-
const runner: LocalCommandRunner = {
|
|
185
|
-
runCommand: async (program, args, options) => {
|
|
186
|
-
received.push({ program, args, options });
|
|
187
|
-
return {
|
|
188
|
-
stdout:
|
|
189
|
-
'\n',
|
|
190
|
-
stderr: '',
|
|
191
|
-
exitCode: 0,
|
|
192
|
-
};
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
const repository = new LocalCommandIssueAttachmentRepository(runner, root);
|
|
196
|
-
|
|
197
|
-
await repository.uploadAttachment({
|
|
198
|
-
issueOrPullRequestUrl: 'https://github.com/owner/repo/issues/1',
|
|
199
|
-
fileName: 'screenshot.png',
|
|
200
|
-
content: new Uint8Array([1]),
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
expect(received).toHaveLength(1);
|
|
204
|
-
expect(received[0].options).toBeUndefined();
|
|
205
|
-
});
|
|
206
189
|
});
|
|
@@ -9,6 +9,10 @@ import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/Loc
|
|
|
9
9
|
|
|
10
10
|
export const UPLOAD_COMMAND = 'upload-file-to-gh-issue';
|
|
11
11
|
|
|
12
|
+
export type IssueAttachmentGithubTokenResolver = (
|
|
13
|
+
issueOrPullRequestUrl: string,
|
|
14
|
+
) => string;
|
|
15
|
+
|
|
12
16
|
export const ALLOWED_ATTACHMENT_EXTENSIONS = [
|
|
13
17
|
'.png',
|
|
14
18
|
'.jpg',
|
|
@@ -57,8 +61,8 @@ export const relabelAttachmentMarkdown = (
|
|
|
57
61
|
export class LocalCommandIssueAttachmentRepository implements IssueAttachmentRepository {
|
|
58
62
|
constructor(
|
|
59
63
|
private readonly localCommandRunner: LocalCommandRunner,
|
|
64
|
+
private readonly resolveGithubToken: IssueAttachmentGithubTokenResolver,
|
|
60
65
|
private readonly temporaryDirectoryRoot: string = tmpdir(),
|
|
61
|
-
private readonly resolveGithubToken?: (repositoryOwner: string) => string,
|
|
62
66
|
) {}
|
|
63
67
|
|
|
64
68
|
async uploadAttachment(
|
|
@@ -73,21 +77,14 @@ export class LocalCommandIssueAttachmentRepository implements IssueAttachmentRep
|
|
|
73
77
|
);
|
|
74
78
|
try {
|
|
75
79
|
await writeFile(filePath, request.content);
|
|
76
|
-
let commandOptions: { env: { GH_TOKEN: string } } | undefined;
|
|
77
|
-
if (this.resolveGithubToken !== undefined) {
|
|
78
|
-
const ownerMatch = request.issueOrPullRequestUrl.match(
|
|
79
|
-
/https:\/\/github\.com\/([A-Za-z0-9._-]+)\//,
|
|
80
|
-
);
|
|
81
|
-
if (ownerMatch !== null) {
|
|
82
|
-
commandOptions = {
|
|
83
|
-
env: { GH_TOKEN: this.resolveGithubToken(ownerMatch[1]) },
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
80
|
const result = await this.localCommandRunner.runCommand(
|
|
88
81
|
UPLOAD_COMMAND,
|
|
89
82
|
[filePath, request.issueOrPullRequestUrl],
|
|
90
|
-
|
|
83
|
+
{
|
|
84
|
+
env: {
|
|
85
|
+
GH_TOKEN: this.resolveGithubToken(request.issueOrPullRequestUrl),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
91
88
|
);
|
|
92
89
|
if (result.exitCode !== 0) {
|
|
93
90
|
throw new Error(
|
|
@@ -1529,4 +1529,146 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1529
1529
|
},
|
|
1530
1530
|
]);
|
|
1531
1531
|
});
|
|
1532
|
+
|
|
1533
|
+
const outputFilePathFor = (sessionName: string, agentId: string): string => {
|
|
1534
|
+
const mainTranscriptPath = mainTranscriptPathFor(sessionName);
|
|
1535
|
+
const slug = path.basename(path.dirname(mainTranscriptPath));
|
|
1536
|
+
const sid = path.basename(mainTranscriptPath, '.jsonl');
|
|
1537
|
+
return path.join(rootDirectory, slug, sid, 'tasks', `${agentId}.output`);
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
const createOutputFile = (sessionName: string, agentId: string): void => {
|
|
1541
|
+
const filePath = outputFilePathFor(sessionName, agentId);
|
|
1542
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1543
|
+
fs.writeFileSync(filePath, '', 'utf8');
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
it('excludes a sub-agent whose output file is absent and whose id matches no running process when the liveness file is absent', async () => {
|
|
1547
|
+
const sessionName = 'session-4161-ghost-agent';
|
|
1548
|
+
const agentId = 'a4161dead00000001';
|
|
1549
|
+
writeAgentTranscript(
|
|
1550
|
+
sessionName,
|
|
1551
|
+
agentId,
|
|
1552
|
+
pendingToolUseEntries('2026-06-27T11:00:00.000Z'),
|
|
1553
|
+
nowEpochSeconds - 600,
|
|
1554
|
+
);
|
|
1555
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1556
|
+
createResolver(),
|
|
1557
|
+
emptyProcessLister(),
|
|
1558
|
+
now,
|
|
1559
|
+
livenessResolverWith(null),
|
|
1560
|
+
rootDirectory,
|
|
1561
|
+
);
|
|
1562
|
+
|
|
1563
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1564
|
+
[sessionName],
|
|
1565
|
+
transcriptMapFor([sessionName]),
|
|
1566
|
+
);
|
|
1567
|
+
|
|
1568
|
+
expect(result.size).toBe(0);
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
it('includes a sub-agent whose output file exists even when the liveness file is absent', async () => {
|
|
1572
|
+
const sessionName = 'session-4161-live-output';
|
|
1573
|
+
const agentId = 'a4161live00000002';
|
|
1574
|
+
writeAgentTranscript(
|
|
1575
|
+
sessionName,
|
|
1576
|
+
agentId,
|
|
1577
|
+
pendingToolUseEntries('2026-06-27T11:45:00.000Z'),
|
|
1578
|
+
nowEpochSeconds - 600,
|
|
1579
|
+
);
|
|
1580
|
+
createOutputFile(sessionName, agentId);
|
|
1581
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1582
|
+
createResolver(),
|
|
1583
|
+
emptyProcessLister(),
|
|
1584
|
+
now,
|
|
1585
|
+
livenessResolverWith(null),
|
|
1586
|
+
rootDirectory,
|
|
1587
|
+
);
|
|
1588
|
+
|
|
1589
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1590
|
+
[sessionName],
|
|
1591
|
+
transcriptMapFor([sessionName]),
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
expect(result.get(sessionName)).toEqual([
|
|
1595
|
+
{
|
|
1596
|
+
label: `agent-${agentId}`,
|
|
1597
|
+
silentSeconds: 600,
|
|
1598
|
+
runningSeconds: 900,
|
|
1599
|
+
waitingOnExternalProcess: false,
|
|
1600
|
+
finishedResultUnconsumed: false,
|
|
1601
|
+
},
|
|
1602
|
+
]);
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
it('includes a sub-agent whose output file is absent but whose id appears in a running process command line when the liveness file is absent', async () => {
|
|
1606
|
+
const sessionName = 'session-4161-live-process';
|
|
1607
|
+
const agentId = 'a4161proc00000003';
|
|
1608
|
+
writeAgentTranscript(
|
|
1609
|
+
sessionName,
|
|
1610
|
+
agentId,
|
|
1611
|
+
pendingToolUseEntries('2026-06-27T11:45:00.000Z'),
|
|
1612
|
+
nowEpochSeconds - 600,
|
|
1613
|
+
);
|
|
1614
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1615
|
+
createResolver(),
|
|
1616
|
+
processListerWith([
|
|
1617
|
+
{
|
|
1618
|
+
commandLine: `/path/to/claude --session ${agentId} --task task.txt`,
|
|
1619
|
+
elapsedSeconds: 600,
|
|
1620
|
+
},
|
|
1621
|
+
]),
|
|
1622
|
+
now,
|
|
1623
|
+
livenessResolverWith(null),
|
|
1624
|
+
rootDirectory,
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1628
|
+
[sessionName],
|
|
1629
|
+
transcriptMapFor([sessionName]),
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
expect(result.get(sessionName)).toEqual([
|
|
1633
|
+
{
|
|
1634
|
+
label: `agent-${agentId}`,
|
|
1635
|
+
silentSeconds: 600,
|
|
1636
|
+
runningSeconds: 900,
|
|
1637
|
+
waitingOnExternalProcess: false,
|
|
1638
|
+
finishedResultUnconsumed: false,
|
|
1639
|
+
},
|
|
1640
|
+
]);
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
it('emits no sub-agent entries when every transcript has no output file and no matching process and the liveness file is absent', async () => {
|
|
1644
|
+
const sessionName = 'session-4161-all-absent';
|
|
1645
|
+
const agentId1 = 'a4161none00000004';
|
|
1646
|
+
const agentId2 = 'a4161none00000005';
|
|
1647
|
+
writeAgentTranscript(
|
|
1648
|
+
sessionName,
|
|
1649
|
+
agentId1,
|
|
1650
|
+
pendingToolUseEntries('2026-06-27T11:00:00.000Z'),
|
|
1651
|
+
nowEpochSeconds - 900,
|
|
1652
|
+
);
|
|
1653
|
+
writeAgentTranscript(
|
|
1654
|
+
sessionName,
|
|
1655
|
+
agentId2,
|
|
1656
|
+
pendingToolUseEntries('2026-06-27T11:00:00.000Z'),
|
|
1657
|
+
nowEpochSeconds - 900,
|
|
1658
|
+
);
|
|
1659
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1660
|
+
createResolver(),
|
|
1661
|
+
emptyProcessLister(),
|
|
1662
|
+
now,
|
|
1663
|
+
livenessResolverWith(null),
|
|
1664
|
+
rootDirectory,
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1668
|
+
[sessionName],
|
|
1669
|
+
transcriptMapFor([sessionName]),
|
|
1670
|
+
);
|
|
1671
|
+
|
|
1672
|
+
expect(result.size).toBe(0);
|
|
1673
|
+
});
|
|
1532
1674
|
});
|
|
@@ -250,6 +250,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
250
250
|
private readonly processLister: SubAgentProcessLister,
|
|
251
251
|
private readonly now: Date,
|
|
252
252
|
private readonly livenessResolver: SubAgentLivenessResolver = alwaysIndeterminateLivenessResolver,
|
|
253
|
+
private readonly runtimeRootDirectory: string | null = null,
|
|
253
254
|
) {}
|
|
254
255
|
|
|
255
256
|
listSubAgentActivitiesBySessionName = async (
|
|
@@ -290,6 +291,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
290
291
|
terminalAgentIds,
|
|
291
292
|
liveSubAgentIds,
|
|
292
293
|
loadNormalizedProcessCommandLines,
|
|
294
|
+
mainTranscriptPath,
|
|
293
295
|
);
|
|
294
296
|
if (activities.length > 0) {
|
|
295
297
|
result.set(sessionName, activities);
|
|
@@ -313,12 +315,34 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
313
315
|
return parseTerminalAgentIds(content);
|
|
314
316
|
};
|
|
315
317
|
|
|
318
|
+
private resolveOutputFilePath = (
|
|
319
|
+
mainTranscriptPath: string | null,
|
|
320
|
+
agentId: string,
|
|
321
|
+
): string | null => {
|
|
322
|
+
if (this.runtimeRootDirectory === null || mainTranscriptPath === null) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const slug = path.basename(path.dirname(mainTranscriptPath));
|
|
326
|
+
const sid = path.basename(mainTranscriptPath, '.jsonl');
|
|
327
|
+
if (slug.length === 0 || slug === '.' || sid.length === 0) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return path.join(
|
|
331
|
+
this.runtimeRootDirectory,
|
|
332
|
+
slug,
|
|
333
|
+
sid,
|
|
334
|
+
'tasks',
|
|
335
|
+
`${agentId}.output`,
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
316
339
|
private collectActivities = async (
|
|
317
340
|
directory: string,
|
|
318
341
|
nowEpochSeconds: number,
|
|
319
342
|
terminalAgentIds: Set<string>,
|
|
320
343
|
liveSubAgentIds: Set<string> | null,
|
|
321
344
|
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
345
|
+
mainTranscriptPath: string | null,
|
|
322
346
|
): Promise<SubAgentActivity[]> => {
|
|
323
347
|
let entries: fs.Dirent[];
|
|
324
348
|
try {
|
|
@@ -339,6 +363,23 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
339
363
|
if (liveSubAgentIds !== null && !liveSubAgentIds.has(agentId)) {
|
|
340
364
|
continue;
|
|
341
365
|
}
|
|
366
|
+
if (liveSubAgentIds === null && this.runtimeRootDirectory !== null) {
|
|
367
|
+
const outputFilePath = this.resolveOutputFilePath(
|
|
368
|
+
mainTranscriptPath,
|
|
369
|
+
agentId,
|
|
370
|
+
);
|
|
371
|
+
const outputFileExists =
|
|
372
|
+
outputFilePath !== null && fs.existsSync(outputFilePath);
|
|
373
|
+
if (!outputFileExists) {
|
|
374
|
+
const processCommandLines = await loadNormalizedProcessCommandLines();
|
|
375
|
+
const appearsInProcess = processCommandLines.some((line) =>
|
|
376
|
+
line.includes(agentId),
|
|
377
|
+
);
|
|
378
|
+
if (!appearsInProcess) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
342
383
|
const filePath = path.join(directory, fileName);
|
|
343
384
|
const activity = await this.toActivity(
|
|
344
385
|
filePath,
|
|
@@ -24,6 +24,7 @@ import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCa
|
|
|
24
24
|
import { StartPreparationUseCase } from './StartPreparationUseCase';
|
|
25
25
|
import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
|
|
26
26
|
import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
|
|
27
|
+
import { TriagerApprovalDispatchUseCase } from './TriagerApprovalDispatchUseCase';
|
|
27
28
|
import { AgentDesignationLabelAdoptUseCase } from './AgentDesignationLabelAdoptUseCase';
|
|
28
29
|
import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
|
|
29
30
|
import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
|
|
@@ -125,6 +126,8 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
125
126
|
mock<RevertOrphanedPreparationUseCase>();
|
|
126
127
|
const mockRevertNotReadyReviewQueueIssueUseCase =
|
|
127
128
|
mock<RevertNotReadyReviewQueueIssueUseCase>();
|
|
129
|
+
const mockTriagerApprovalDispatchUseCase =
|
|
130
|
+
mock<TriagerApprovalDispatchUseCase>();
|
|
128
131
|
const mockAgentDesignationLabelAdoptUseCase =
|
|
129
132
|
mock<AgentDesignationLabelAdoptUseCase>();
|
|
130
133
|
const mockUpdateRateLimitCacheUseCase = mock<UpdateRateLimitCacheUseCase>();
|
|
@@ -156,6 +159,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
156
159
|
mockStartPreparationUseCase,
|
|
157
160
|
mockRevertOrphanedPreparationUseCase,
|
|
158
161
|
mockRevertNotReadyReviewQueueIssueUseCase,
|
|
162
|
+
mockTriagerApprovalDispatchUseCase,
|
|
159
163
|
mockAgentDesignationLabelAdoptUseCase,
|
|
160
164
|
mockUpdateRateLimitCacheUseCase,
|
|
161
165
|
mockDailySecurityScanUseCase,
|
|
@@ -236,6 +240,29 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
236
240
|
});
|
|
237
241
|
});
|
|
238
242
|
|
|
243
|
+
it('should call TriagerApprovalDispatchUseCase with project URL', async () => {
|
|
244
|
+
const input = {
|
|
245
|
+
projectName: 'test-project',
|
|
246
|
+
org: 'test-org',
|
|
247
|
+
projectUrl: 'https://github.com/test-org/test-project',
|
|
248
|
+
manager: 'test-manager',
|
|
249
|
+
workingReport: {
|
|
250
|
+
repo: 'test-repo',
|
|
251
|
+
members: ['member1'],
|
|
252
|
+
spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
|
|
253
|
+
},
|
|
254
|
+
urlOfStoryView: 'https://github.com/test-org/test-project/issues',
|
|
255
|
+
disabled: false,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
await useCase.run(input);
|
|
259
|
+
expect(mockTriagerApprovalDispatchUseCase.run).toHaveBeenCalledWith(
|
|
260
|
+
expect.objectContaining({
|
|
261
|
+
projectUrl: 'https://github.com/test-org/test-project',
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
239
266
|
it('should return null and skip all processing when disabled is true', async () => {
|
|
240
267
|
const input = {
|
|
241
268
|
projectName: 'test-project',
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
import { AgentDesignationLabelAdoptUseCase } from './AgentDesignationLabelAdoptUseCase';
|
|
29
29
|
import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
|
|
30
30
|
import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
|
|
31
|
+
import { TriagerApprovalDispatchUseCase } from './TriagerApprovalDispatchUseCase';
|
|
32
|
+
import { isRecord } from './isRecord';
|
|
31
33
|
import { resolveLabelsAsLlmAgentName } from './resolveLabelsAsLlmAgentName';
|
|
32
34
|
import { resolveAllowedIssueAuthors } from './resolveAllowedIssueAuthors';
|
|
33
35
|
import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
|
|
@@ -64,9 +66,6 @@ const isTransientApiError = (error: Error): boolean => {
|
|
|
64
66
|
const TRANSIENT_NETWORK_ERROR_CODE_PATTERN =
|
|
65
67
|
/\b(ECONNRESET|ECONNREFUSED|ECONNABORTED|ETIMEDOUT|EPIPE|ENOTFOUND|EAI_AGAIN|ERR_NETWORK|ERR_SOCKET_CONNECTION_TIMEOUT)\b/;
|
|
66
68
|
|
|
67
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
68
|
-
typeof value === 'object' && value !== null;
|
|
69
|
-
|
|
70
69
|
const extractHttpStatusFromError = (error: Error): number | null => {
|
|
71
70
|
if (!isRecord(error)) {
|
|
72
71
|
return null;
|
|
@@ -137,6 +136,7 @@ export class HandleScheduledEventUseCase {
|
|
|
137
136
|
readonly startPreparationUseCase: StartPreparationUseCase,
|
|
138
137
|
readonly revertOrphanedPreparationUseCase: RevertOrphanedPreparationUseCase,
|
|
139
138
|
readonly revertNotReadyReviewQueueIssueUseCase: RevertNotReadyReviewQueueIssueUseCase,
|
|
139
|
+
readonly triagerApprovalDispatchUseCase: TriagerApprovalDispatchUseCase,
|
|
140
140
|
readonly agentDesignationLabelAdoptUseCase: AgentDesignationLabelAdoptUseCase,
|
|
141
141
|
readonly updateRateLimitCacheUseCase: UpdateRateLimitCacheUseCase | null,
|
|
142
142
|
readonly dailySecurityScanUseCase: DailySecurityScanUseCase | null,
|
|
@@ -437,6 +437,10 @@ ${JSON.stringify(e)}
|
|
|
437
437
|
changeTargetPathAliases: input.changeTargetPathAliases,
|
|
438
438
|
allowedIssueAuthors,
|
|
439
439
|
});
|
|
440
|
+
await this.triagerApprovalDispatchUseCase.run({
|
|
441
|
+
projectUrl: input.projectUrl,
|
|
442
|
+
allowedIssueAuthors,
|
|
443
|
+
});
|
|
440
444
|
if (this.dailySecurityScanUseCase !== null && input.dailySecurityScan) {
|
|
441
445
|
await this.dailySecurityScanUseCase.run({
|
|
442
446
|
targetDates: targetDateTimes,
|