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.
Files changed (72) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +15 -0
  3. package/bin/adapter/entry-points/cli/index.js +5 -3
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js +16 -5
  6. package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js.map +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/{index-BcnLfodS.js → index-g4SqIzCh.js} +2 -2
  8. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  9. package/bin/adapter/entry-points/console/webServer.js +16 -2
  10. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  11. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
  12. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  13. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +1 -1
  14. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  15. package/bin/adapter/repositories/LocalCommandIssueAttachmentRepository.js +7 -12
  16. package/bin/adapter/repositories/LocalCommandIssueAttachmentRepository.js.map +1 -1
  17. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +26 -3
  18. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
  19. package/bin/domain/usecases/HandleScheduledEventUseCase.js +10 -5
  20. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  21. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +142 -0
  22. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -0
  23. package/bin/domain/usecases/agentReportPrefix.js +5 -0
  24. package/bin/domain/usecases/agentReportPrefix.js.map +1 -0
  25. package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js +2 -2
  26. package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js.map +1 -1
  27. package/bin/domain/usecases/isRecord.js +6 -0
  28. package/bin/domain/usecases/isRecord.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/adapter/entry-points/cli/index.ts +8 -4
  31. package/src/adapter/entry-points/console/consoleGithubTokenResolver.test.ts +29 -2
  32. package/src/adapter/entry-points/console/consoleGithubTokenResolver.ts +21 -4
  33. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +5 -1
  34. package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.test.ts +8 -5
  35. package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.ts +2 -2
  36. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +2 -2
  37. package/src/adapter/entry-points/console/ui-dist/assets/{index-BcnLfodS.js → index-g4SqIzCh.js} +2 -2
  38. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  39. package/src/adapter/entry-points/console/webServer.test.ts +56 -5
  40. package/src/adapter/entry-points/console/webServer.ts +20 -3
  41. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -0
  42. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -0
  43. package/src/adapter/repositories/LocalCommandIssueAttachmentRepository.test.ts +53 -70
  44. package/src/adapter/repositories/LocalCommandIssueAttachmentRepository.ts +10 -13
  45. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +142 -0
  46. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +41 -0
  47. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +27 -0
  48. package/src/domain/usecases/HandleScheduledEventUseCase.ts +7 -3
  49. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +879 -0
  50. package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +247 -0
  51. package/src/domain/usecases/agentReportPrefix.ts +1 -0
  52. package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +1 -1
  53. package/src/domain/usecases/isRecord.ts +2 -0
  54. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  55. package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts +1 -0
  56. package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts.map +1 -1
  57. package/types/adapter/entry-points/console/webServer.d.ts +2 -1
  58. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  59. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  60. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  61. package/types/adapter/repositories/LocalCommandIssueAttachmentRepository.d.ts +3 -2
  62. package/types/adapter/repositories/LocalCommandIssueAttachmentRepository.d.ts.map +1 -1
  63. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +3 -1
  64. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
  65. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
  66. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  67. package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts +15 -0
  68. package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -0
  69. package/types/domain/usecases/agentReportPrefix.d.ts +2 -0
  70. package/types/domain/usecases/agentReportPrefix.d.ts.map +1 -0
  71. package/types/domain/usecases/isRecord.d.ts +2 -0
  72. 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
+ '![attachment](https://github.com/user-attachments/assets/66666666-7777-8888-9999-000000000000)\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(runner, root);
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(runner, root);
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(runner, root);
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
- '![screenshot](https://github.com/user-attachments/assets/abc)\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
- '![screenshot](https://github.com/user-attachments/assets/abc)\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
- commandOptions,
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,