github-issue-tower-defence-management 1.122.2 → 1.122.4
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 +15 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/cli/index.js +9 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -5
- package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +78 -39
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +8 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +5 -1
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +2 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.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/ConfigurableSilentSessionMessageComposer.js +6 -7
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js +60 -11
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js.map +1 -1
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +64 -7
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +23 -4
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +5 -7
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +51 -16
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +3 -1
- package/src/adapter/entry-points/cli/index.ts +11 -0
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +38 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +29 -4
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +190 -11
- package/src/adapter/entry-points/console/consoleOperationApi.ts +97 -56
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +13 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +11 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +3 -0
- package/src/adapter/entry-points/console/webServer.test.ts +4 -0
- package/src/adapter/entry-points/console/webServer.ts +9 -1
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +72 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +3 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +60 -37
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -18
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.test.ts +223 -0
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.ts +94 -10
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +12 -2
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +196 -4
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +91 -7
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +86 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +34 -7
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +112 -51
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +8 -17
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +250 -18
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +72 -22
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -7
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +2 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.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/consoleListsWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -3
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts +13 -2
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +5 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +2 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -3
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +2 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +4 -4
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
|
|
4
4
|
import { SessionSubAgentActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository';
|
|
5
|
+
import { SubAgentProcessLister } from '../../domain/usecases/adapter-interfaces/SubAgentProcessLister';
|
|
5
6
|
import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
|
|
6
7
|
|
|
7
8
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -23,9 +24,18 @@ const parseEpochSeconds = (timestamp: string | null): number | null => {
|
|
|
23
24
|
return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000);
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
const PENDING_TOOL_COMMAND_FRAGMENT_LENGTH = 60;
|
|
28
|
+
|
|
29
|
+
export const normalizeCommandFragment = (command: string): string =>
|
|
30
|
+
command
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.trim()
|
|
33
|
+
.slice(0, PENDING_TOOL_COMMAND_FRAGMENT_LENGTH);
|
|
34
|
+
|
|
26
35
|
type ParsedTranscript = {
|
|
27
36
|
firstEntryEpochSeconds: number | null;
|
|
28
37
|
lastEntryIndicatesCompletion: boolean;
|
|
38
|
+
pendingToolCommands: string[];
|
|
29
39
|
};
|
|
30
40
|
|
|
31
41
|
const readContentBlocks = (
|
|
@@ -65,9 +75,33 @@ const entryIndicatesCompletion = (entry: Record<string, unknown>): boolean => {
|
|
|
65
75
|
return false;
|
|
66
76
|
};
|
|
67
77
|
|
|
78
|
+
const entryPendingToolCommands = (entry: Record<string, unknown>): string[] => {
|
|
79
|
+
const type = readString(entry, 'type');
|
|
80
|
+
const message = entry.message;
|
|
81
|
+
if (type !== 'assistant' || !isRecord(message)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const commands: string[] = [];
|
|
85
|
+
for (const block of readContentBlocks(message)) {
|
|
86
|
+
if (readString(block, 'type') !== 'tool_use') {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const input = block.input;
|
|
90
|
+
if (!isRecord(input)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const command = readString(input, 'command');
|
|
94
|
+
if (command !== null && command.trim().length > 0) {
|
|
95
|
+
commands.push(command);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return commands;
|
|
99
|
+
};
|
|
100
|
+
|
|
68
101
|
const parseTranscript = (content: string): ParsedTranscript => {
|
|
69
102
|
let firstEntryEpochSeconds: number | null = null;
|
|
70
103
|
let lastEntryIndicatesCompletion = false;
|
|
104
|
+
let pendingToolCommands: string[] = [];
|
|
71
105
|
for (const line of content.split('\n')) {
|
|
72
106
|
const trimmed = line.trim();
|
|
73
107
|
if (trimmed.length === 0) {
|
|
@@ -88,9 +122,14 @@ const parseTranscript = (content: string): ParsedTranscript => {
|
|
|
88
122
|
}
|
|
89
123
|
if (isRecord(parsed.message)) {
|
|
90
124
|
lastEntryIndicatesCompletion = entryIndicatesCompletion(parsed);
|
|
125
|
+
pendingToolCommands = entryPendingToolCommands(parsed);
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
firstEntryEpochSeconds,
|
|
130
|
+
lastEntryIndicatesCompletion,
|
|
131
|
+
pendingToolCommands,
|
|
132
|
+
};
|
|
94
133
|
};
|
|
95
134
|
|
|
96
135
|
const clampToZero = (value: number): number => (value > 0 ? value : 0);
|
|
@@ -98,6 +137,7 @@ const clampToZero = (value: number): number => (value > 0 ? value : 0);
|
|
|
98
137
|
export class TranscriptSessionSubAgentActivityRepository implements SessionSubAgentActivityRepository {
|
|
99
138
|
constructor(
|
|
100
139
|
private readonly directoryResolver: SubAgentTranscriptDirectoryResolver,
|
|
140
|
+
private readonly processLister: SubAgentProcessLister,
|
|
101
141
|
private readonly now: Date,
|
|
102
142
|
) {}
|
|
103
143
|
|
|
@@ -107,6 +147,16 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
107
147
|
): Promise<Map<string, SubAgentActivity[]>> => {
|
|
108
148
|
const result = new Map<string, SubAgentActivity[]>();
|
|
109
149
|
const nowEpochSeconds = Math.floor(this.now.getTime() / 1000);
|
|
150
|
+
let normalizedProcessCommandLines: string[] | null = null;
|
|
151
|
+
const loadNormalizedProcessCommandLines = async (): Promise<string[]> => {
|
|
152
|
+
if (normalizedProcessCommandLines === null) {
|
|
153
|
+
const processes = await this.processLister.listProcesses();
|
|
154
|
+
normalizedProcessCommandLines = processes.map((process) =>
|
|
155
|
+
process.commandLine.replace(/\s+/g, ' ').trim(),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return normalizedProcessCommandLines;
|
|
159
|
+
};
|
|
110
160
|
for (const sessionName of sessionNames) {
|
|
111
161
|
const mainTranscriptPath =
|
|
112
162
|
transcriptPathBySessionName.get(sessionName) ?? null;
|
|
@@ -117,7 +167,11 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
117
167
|
if (directory === null) {
|
|
118
168
|
continue;
|
|
119
169
|
}
|
|
120
|
-
const activities = this.collectActivities(
|
|
170
|
+
const activities = await this.collectActivities(
|
|
171
|
+
directory,
|
|
172
|
+
nowEpochSeconds,
|
|
173
|
+
loadNormalizedProcessCommandLines,
|
|
174
|
+
);
|
|
121
175
|
if (activities.length > 0) {
|
|
122
176
|
result.set(sessionName, activities);
|
|
123
177
|
}
|
|
@@ -125,10 +179,11 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
125
179
|
return result;
|
|
126
180
|
};
|
|
127
181
|
|
|
128
|
-
private collectActivities = (
|
|
182
|
+
private collectActivities = async (
|
|
129
183
|
directory: string,
|
|
130
184
|
nowEpochSeconds: number,
|
|
131
|
-
|
|
185
|
+
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
186
|
+
): Promise<SubAgentActivity[]> => {
|
|
132
187
|
let entries: fs.Dirent[];
|
|
133
188
|
try {
|
|
134
189
|
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
@@ -142,7 +197,12 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
142
197
|
continue;
|
|
143
198
|
}
|
|
144
199
|
const filePath = path.join(directory, fileName);
|
|
145
|
-
const activity = this.toActivity(
|
|
200
|
+
const activity = await this.toActivity(
|
|
201
|
+
filePath,
|
|
202
|
+
fileName,
|
|
203
|
+
nowEpochSeconds,
|
|
204
|
+
loadNormalizedProcessCommandLines,
|
|
205
|
+
);
|
|
146
206
|
if (activity !== null) {
|
|
147
207
|
activities.push(activity);
|
|
148
208
|
}
|
|
@@ -150,11 +210,12 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
150
210
|
return activities;
|
|
151
211
|
};
|
|
152
212
|
|
|
153
|
-
private toActivity = (
|
|
213
|
+
private toActivity = async (
|
|
154
214
|
filePath: string,
|
|
155
215
|
fileName: string,
|
|
156
216
|
nowEpochSeconds: number,
|
|
157
|
-
|
|
217
|
+
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
218
|
+
): Promise<SubAgentActivity | null> => {
|
|
158
219
|
let content: string;
|
|
159
220
|
let stats: fs.Stats;
|
|
160
221
|
try {
|
|
@@ -178,6 +239,29 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
178
239
|
label: fileName.replace(/\.jsonl$/, ''),
|
|
179
240
|
silentSeconds,
|
|
180
241
|
runningSeconds,
|
|
242
|
+
waitingOnExternalProcess: await this.hasLiveMatchingProcess(
|
|
243
|
+
transcript.pendingToolCommands,
|
|
244
|
+
loadNormalizedProcessCommandLines,
|
|
245
|
+
),
|
|
181
246
|
};
|
|
182
247
|
};
|
|
248
|
+
|
|
249
|
+
private hasLiveMatchingProcess = async (
|
|
250
|
+
pendingToolCommands: string[],
|
|
251
|
+
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
252
|
+
): Promise<boolean> => {
|
|
253
|
+
if (pendingToolCommands.length === 0) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const fragments = pendingToolCommands
|
|
257
|
+
.map(normalizeCommandFragment)
|
|
258
|
+
.filter((fragment) => fragment.length > 0);
|
|
259
|
+
if (fragments.length === 0) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const processCommandLines = await loadNormalizedProcessCommandLines();
|
|
263
|
+
return fragments.some((fragment) =>
|
|
264
|
+
processCommandLines.some((commandLine) => commandLine.includes(fragment)),
|
|
265
|
+
);
|
|
266
|
+
};
|
|
183
267
|
}
|
|
@@ -593,6 +593,92 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
593
593
|
});
|
|
594
594
|
});
|
|
595
595
|
|
|
596
|
+
describe('getCachedProject', () => {
|
|
597
|
+
it('returns the daemon-cached project without any GraphQL project load', async () => {
|
|
598
|
+
const { repository, localStorageCacheRepository, projectRepository } =
|
|
599
|
+
createApiV3CheerioRestIssueRepository();
|
|
600
|
+
const cachedProject = buildTestProject('cached-project');
|
|
601
|
+
localStorageCacheRepository.getSingle.mockResolvedValue({
|
|
602
|
+
lastFetchedAt: '2026-07-07T00:30:00.000Z',
|
|
603
|
+
lastFullFetchAt: '2026-07-07T00:00:00.000Z',
|
|
604
|
+
project: cachedProject,
|
|
605
|
+
issues: [],
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const result = await repository.getCachedProject('cached-project');
|
|
609
|
+
|
|
610
|
+
expect(result).toEqual(cachedProject);
|
|
611
|
+
expect(localStorageCacheRepository.getSingle).toHaveBeenCalledWith(
|
|
612
|
+
'allIssues-cached-project',
|
|
613
|
+
);
|
|
614
|
+
expect(projectRepository.getProject).not.toHaveBeenCalled();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('returns null on a cache miss so the caller can fall back to GraphQL', async () => {
|
|
618
|
+
const { repository, localStorageCacheRepository, projectRepository } =
|
|
619
|
+
createApiV3CheerioRestIssueRepository();
|
|
620
|
+
localStorageCacheRepository.getSingle.mockResolvedValue(null);
|
|
621
|
+
|
|
622
|
+
const result = await repository.getCachedProject('missing-project');
|
|
623
|
+
|
|
624
|
+
expect(result).toBeNull();
|
|
625
|
+
expect(projectRepository.getProject).not.toHaveBeenCalled();
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe('updateNextActionDate', () => {
|
|
630
|
+
const projectWithNextActionDate = (): Project => ({
|
|
631
|
+
...buildTestProject('nad-project'),
|
|
632
|
+
nextActionDate: { name: 'Next Action Date', fieldId: 'nad-field' },
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('uses the provided project item id and skips the GraphQL item fetch', async () => {
|
|
636
|
+
const { repository, graphqlProjectItemRepository } =
|
|
637
|
+
createApiV3CheerioRestIssueRepository();
|
|
638
|
+
graphqlProjectItemRepository.updateProjectField.mockResolvedValue();
|
|
639
|
+
|
|
640
|
+
await repository.updateNextActionDate(
|
|
641
|
+
'https://github.com/o/r/issues/1',
|
|
642
|
+
projectWithNextActionDate(),
|
|
643
|
+
new Date('2026-07-20T00:00:00.000Z'),
|
|
644
|
+
'given-item-id',
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
expect(
|
|
648
|
+
graphqlProjectItemRepository.fetchProjectItemByUrl,
|
|
649
|
+
).not.toHaveBeenCalled();
|
|
650
|
+
expect(
|
|
651
|
+
graphqlProjectItemRepository.updateProjectField,
|
|
652
|
+
).toHaveBeenCalledWith('nad-project', 'nad-field', 'given-item-id', {
|
|
653
|
+
date: '2026-07-20',
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('falls back to fetchProjectItemByUrl when no project item id is provided', async () => {
|
|
658
|
+
const { repository, graphqlProjectItemRepository } =
|
|
659
|
+
createApiV3CheerioRestIssueRepository();
|
|
660
|
+
graphqlProjectItemRepository.updateProjectField.mockResolvedValue();
|
|
661
|
+
graphqlProjectItemRepository.fetchProjectItemByUrl.mockResolvedValue(
|
|
662
|
+
buildProjectItem('https://github.com/o/r/issues/1', 'fallback'),
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
await repository.updateNextActionDate(
|
|
666
|
+
'https://github.com/o/r/issues/1',
|
|
667
|
+
projectWithNextActionDate(),
|
|
668
|
+
new Date('2026-07-20T00:00:00.000Z'),
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
expect(
|
|
672
|
+
graphqlProjectItemRepository.fetchProjectItemByUrl,
|
|
673
|
+
).toHaveBeenCalledWith('https://github.com/o/r/issues/1', 'nad-project');
|
|
674
|
+
expect(
|
|
675
|
+
graphqlProjectItemRepository.updateProjectField,
|
|
676
|
+
).toHaveBeenCalledWith('nad-project', 'nad-field', 'item-fallback', {
|
|
677
|
+
date: '2026-07-20',
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
596
682
|
describe('setDependedIssueUrl', () => {
|
|
597
683
|
const projectWithDependedIssueUrlField = {
|
|
598
684
|
...mock<Project>(),
|
|
@@ -680,6 +680,25 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
680
680
|
};
|
|
681
681
|
};
|
|
682
682
|
|
|
683
|
+
// Reads the Project (status/story option ids and field ids) that the TDPM
|
|
684
|
+
// daemon persisted into the `allIssues-${projectId}` cache, without any
|
|
685
|
+
// GraphQL call. Returns null on cache miss so callers can fall back to a
|
|
686
|
+
// GraphQL project load only when the daemon has not populated the cache yet.
|
|
687
|
+
getCachedProject = async (
|
|
688
|
+
projectId: Project['id'],
|
|
689
|
+
): Promise<Project | null> => {
|
|
690
|
+
const raw = await this.localStorageCacheRepository.getSingle(
|
|
691
|
+
`allIssues-${projectId}`,
|
|
692
|
+
);
|
|
693
|
+
if (typeof raw !== 'object' || raw === null || !('project' in raw)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
if (!typia.is<Project>(raw.project)) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
return raw.project;
|
|
700
|
+
};
|
|
701
|
+
|
|
683
702
|
private toDateString = (date: Date): string =>
|
|
684
703
|
`${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;
|
|
685
704
|
|
|
@@ -855,22 +874,30 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
855
874
|
issueUrl: string,
|
|
856
875
|
project: Project,
|
|
857
876
|
date: Date,
|
|
877
|
+
projectItemId?: string,
|
|
858
878
|
): Promise<void> => {
|
|
859
879
|
if (!project.nextActionDate) {
|
|
860
880
|
return;
|
|
861
881
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
882
|
+
// When the caller already knows the project item id (e.g. the console,
|
|
883
|
+
// which receives it in the request body), use it directly and skip the
|
|
884
|
+
// GraphQL fetchProjectItemByUrl lookup. Fall back to the lookup only when
|
|
885
|
+
// no id was supplied, preserving the original behavior for other callers.
|
|
886
|
+
const itemId =
|
|
887
|
+
projectItemId ??
|
|
888
|
+
(
|
|
889
|
+
await this.graphqlProjectItemRepository.fetchProjectItemByUrl(
|
|
890
|
+
issueUrl,
|
|
891
|
+
project.id,
|
|
892
|
+
)
|
|
893
|
+
)?.id;
|
|
894
|
+
if (!itemId) {
|
|
868
895
|
return;
|
|
869
896
|
}
|
|
870
897
|
return this.graphqlProjectItemRepository.updateProjectField(
|
|
871
898
|
project.id,
|
|
872
899
|
project.nextActionDate.fieldId,
|
|
873
|
-
|
|
900
|
+
itemId,
|
|
874
901
|
{ date: date.toISOString().split('T')[0] },
|
|
875
902
|
);
|
|
876
903
|
};
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { DefaultSilentSessionMessageComposer } from './DefaultSilentSessionMessageComposer';
|
|
2
2
|
import { SILENT_SESSION_REMINDER_SENTINEL } from './silentSessionReminderSentinel';
|
|
3
3
|
|
|
4
|
-
const THRESHOLDS = {
|
|
5
|
-
subAgentSilentThresholdSeconds: 300,
|
|
6
|
-
subAgentRunningThresholdSeconds: 900,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
4
|
describe('DefaultSilentSessionMessageComposer', () => {
|
|
10
5
|
const composer = new DefaultSilentSessionMessageComposer();
|
|
11
6
|
|
|
@@ -15,10 +10,16 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
15
10
|
});
|
|
16
11
|
|
|
17
12
|
it('embeds the reminder sentinel in the sub-agent section', () => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
const subAgent = {
|
|
14
|
+
label: 'sub-process-1',
|
|
15
|
+
silentSeconds: 360,
|
|
16
|
+
runningSeconds: 1200,
|
|
17
|
+
waitingOnExternalProcess: false,
|
|
18
|
+
};
|
|
19
|
+
const section = composer.composeSubAgentSection({
|
|
20
|
+
idleSubAgents: [subAgent],
|
|
21
|
+
longRunningSubAgents: [subAgent],
|
|
22
|
+
});
|
|
22
23
|
expect(section).toContain(SILENT_SESSION_REMINDER_SENTINEL);
|
|
23
24
|
});
|
|
24
25
|
|
|
@@ -116,10 +117,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
it('emits a distinct idle message for a sub-agent that is only output-idle', () => {
|
|
119
|
-
const section = composer.composeSubAgentSection(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
const section = composer.composeSubAgentSection({
|
|
121
|
+
idleSubAgents: [
|
|
122
|
+
{
|
|
123
|
+
label: 'sub-process-idle',
|
|
124
|
+
silentSeconds: 360,
|
|
125
|
+
runningSeconds: 60,
|
|
126
|
+
waitingOnExternalProcess: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
longRunningSubAgents: [],
|
|
130
|
+
});
|
|
123
131
|
expect(section).toContain('sub-process-idle');
|
|
124
132
|
expect(section).toContain('no output for 6m');
|
|
125
133
|
expect(section).toContain('restart, hand off, or replace it');
|
|
@@ -128,10 +136,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
128
136
|
});
|
|
129
137
|
|
|
130
138
|
it('presents the system-detected idle duration as the authoritative signal in the idle message', () => {
|
|
131
|
-
const section = composer.composeSubAgentSection(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
const section = composer.composeSubAgentSection({
|
|
140
|
+
idleSubAgents: [
|
|
141
|
+
{
|
|
142
|
+
label: 'sub-process-idle',
|
|
143
|
+
silentSeconds: 360,
|
|
144
|
+
runningSeconds: 60,
|
|
145
|
+
waitingOnExternalProcess: false,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
longRunningSubAgents: [],
|
|
149
|
+
});
|
|
135
150
|
expect(section).toContain(
|
|
136
151
|
"The system has already detected, from each sub-process's last tool activity, that it has produced no output for about the minutes shown below.",
|
|
137
152
|
);
|
|
@@ -142,10 +157,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
142
157
|
});
|
|
143
158
|
|
|
144
159
|
it('forbids speculation and dismissal without evidence in the idle message', () => {
|
|
145
|
-
const section = composer.composeSubAgentSection(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
const section = composer.composeSubAgentSection({
|
|
161
|
+
idleSubAgents: [
|
|
162
|
+
{
|
|
163
|
+
label: 'sub-process-idle',
|
|
164
|
+
silentSeconds: 360,
|
|
165
|
+
runningSeconds: 60,
|
|
166
|
+
waitingOnExternalProcess: false,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
longRunningSubAgents: [],
|
|
170
|
+
});
|
|
149
171
|
expect(section).toContain(
|
|
150
172
|
'Around five minutes of silence is a real warning of a possible hang',
|
|
151
173
|
);
|
|
@@ -155,10 +177,17 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
155
177
|
});
|
|
156
178
|
|
|
157
179
|
it('requires a concrete cause-check covering recent activity and external-dependency waiting in the idle message', () => {
|
|
158
|
-
const section = composer.composeSubAgentSection(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
const section = composer.composeSubAgentSection({
|
|
181
|
+
idleSubAgents: [
|
|
182
|
+
{
|
|
183
|
+
label: 'sub-process-idle',
|
|
184
|
+
silentSeconds: 360,
|
|
185
|
+
runningSeconds: 60,
|
|
186
|
+
waitingOnExternalProcess: false,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
longRunningSubAgents: [],
|
|
190
|
+
});
|
|
162
191
|
expect(section).toContain('determine the CAUSE by a concrete check');
|
|
163
192
|
expect(section).toContain(
|
|
164
193
|
'a very recent push or commit, or output from any nested sub-processes this sub-process itself started',
|
|
@@ -175,26 +204,34 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
175
204
|
});
|
|
176
205
|
|
|
177
206
|
it('requires logging the investigation result even though owner notification is not required', () => {
|
|
178
|
-
const section = composer.composeSubAgentSection(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
207
|
+
const section = composer.composeSubAgentSection({
|
|
208
|
+
idleSubAgents: [
|
|
209
|
+
{
|
|
210
|
+
label: 'sub-process-idle',
|
|
211
|
+
silentSeconds: 360,
|
|
212
|
+
runningSeconds: 60,
|
|
213
|
+
waitingOnExternalProcess: false,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
longRunningSubAgents: [],
|
|
217
|
+
});
|
|
182
218
|
expect(section).toContain(
|
|
183
219
|
'Owner notification is not required, but you MUST output your investigation result in this session so it remains as a log.',
|
|
184
220
|
);
|
|
185
221
|
});
|
|
186
222
|
|
|
187
223
|
it('emits a distinct long-running message for a sub-agent that has only run too long', () => {
|
|
188
|
-
const section = composer.composeSubAgentSection(
|
|
189
|
-
[
|
|
224
|
+
const section = composer.composeSubAgentSection({
|
|
225
|
+
idleSubAgents: [],
|
|
226
|
+
longRunningSubAgents: [
|
|
190
227
|
{
|
|
191
228
|
label: 'sub-process-long',
|
|
192
229
|
silentSeconds: 30,
|
|
193
230
|
runningSeconds: 1200,
|
|
231
|
+
waitingOnExternalProcess: false,
|
|
194
232
|
},
|
|
195
233
|
],
|
|
196
|
-
|
|
197
|
-
);
|
|
234
|
+
});
|
|
198
235
|
expect(section).toContain('sub-process-long');
|
|
199
236
|
expect(section).toContain('running for 20m');
|
|
200
237
|
expect(section).toContain('infinite loop');
|
|
@@ -209,25 +246,32 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
209
246
|
});
|
|
210
247
|
|
|
211
248
|
it('does not foreground the short idle time in the long-running message', () => {
|
|
212
|
-
const section = composer.composeSubAgentSection(
|
|
213
|
-
[
|
|
249
|
+
const section = composer.composeSubAgentSection({
|
|
250
|
+
idleSubAgents: [],
|
|
251
|
+
longRunningSubAgents: [
|
|
214
252
|
{
|
|
215
253
|
label: 'sub-process-long',
|
|
216
254
|
silentSeconds: 30,
|
|
217
255
|
runningSeconds: 1200,
|
|
256
|
+
waitingOnExternalProcess: false,
|
|
218
257
|
},
|
|
219
258
|
],
|
|
220
|
-
|
|
221
|
-
);
|
|
259
|
+
});
|
|
222
260
|
expect(section).not.toContain('no output for');
|
|
223
261
|
expect(section).not.toContain('silent for');
|
|
224
262
|
});
|
|
225
263
|
|
|
226
264
|
it('emits both distinct messages for a sub-agent matching both conditions, kept separate', () => {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
265
|
+
const subAgent = {
|
|
266
|
+
label: 'sub-process-both',
|
|
267
|
+
silentSeconds: 360,
|
|
268
|
+
runningSeconds: 1200,
|
|
269
|
+
waitingOnExternalProcess: false,
|
|
270
|
+
};
|
|
271
|
+
const section = composer.composeSubAgentSection({
|
|
272
|
+
idleSubAgents: [subAgent],
|
|
273
|
+
longRunningSubAgents: [subAgent],
|
|
274
|
+
});
|
|
231
275
|
expect(section).toContain('no output for 6m');
|
|
232
276
|
expect(section).toContain('determine the CAUSE by a concrete check');
|
|
233
277
|
expect(section).toContain('running for 20m');
|
|
@@ -238,13 +282,24 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
238
282
|
});
|
|
239
283
|
|
|
240
284
|
it('groups each sub-agent under the condition it matched', () => {
|
|
241
|
-
const section = composer.composeSubAgentSection(
|
|
242
|
-
[
|
|
243
|
-
{
|
|
244
|
-
|
|
285
|
+
const section = composer.composeSubAgentSection({
|
|
286
|
+
idleSubAgents: [
|
|
287
|
+
{
|
|
288
|
+
label: 'idle-only',
|
|
289
|
+
silentSeconds: 360,
|
|
290
|
+
runningSeconds: 60,
|
|
291
|
+
waitingOnExternalProcess: false,
|
|
292
|
+
},
|
|
245
293
|
],
|
|
246
|
-
|
|
247
|
-
|
|
294
|
+
longRunningSubAgents: [
|
|
295
|
+
{
|
|
296
|
+
label: 'long-only',
|
|
297
|
+
silentSeconds: 10,
|
|
298
|
+
runningSeconds: 960,
|
|
299
|
+
waitingOnExternalProcess: false,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
248
303
|
const [idleSection, longRunningSection] = section.split('\n\n');
|
|
249
304
|
expect(idleSection).toContain('idle-only');
|
|
250
305
|
expect(idleSection).not.toContain('long-only');
|
|
@@ -254,10 +309,16 @@ describe('DefaultSilentSessionMessageComposer', () => {
|
|
|
254
309
|
|
|
255
310
|
it('does not contain any host-specific or internal identifiers', () => {
|
|
256
311
|
const mainSection = composer.composeMainStalledSection(600);
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
312
|
+
const subAgent = {
|
|
313
|
+
label: 'sub-process-1',
|
|
314
|
+
silentSeconds: 360,
|
|
315
|
+
runningSeconds: 1200,
|
|
316
|
+
waitingOnExternalProcess: false,
|
|
317
|
+
};
|
|
318
|
+
const subSection = composer.composeSubAgentSection({
|
|
319
|
+
idleSubAgents: [subAgent],
|
|
320
|
+
longRunningSubAgents: [subAgent],
|
|
321
|
+
});
|
|
261
322
|
const combined = `${mainSection}\n${subSection}`.toLowerCase();
|
|
262
323
|
expect(combined).not.toContain('claude');
|
|
263
324
|
expect(combined).not.toContain('take ownership');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SubAgentActivity } from '../entities/LiveSessionActivitySnapshot';
|
|
2
2
|
import {
|
|
3
3
|
SilentSessionMessageComposer,
|
|
4
|
-
|
|
4
|
+
SubAgentStallSections,
|
|
5
5
|
} from './adapter-interfaces/SilentSessionMessageComposer';
|
|
6
6
|
import { SILENT_SESSION_REMINDER_SENTINEL } from './silentSessionReminderSentinel';
|
|
7
7
|
|
|
@@ -74,24 +74,15 @@ export class DefaultSilentSessionMessageComposer implements SilentSessionMessage
|
|
|
74
74
|
return composeMainStalledMessage(mainSilentSeconds, this.ownerCallMarker);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
composeSubAgentSection = (
|
|
78
|
-
subAgents: SubAgentActivity[],
|
|
79
|
-
thresholds: SubAgentStallThresholds,
|
|
80
|
-
): string => {
|
|
81
|
-
const idleSubAgents = subAgents.filter(
|
|
82
|
-
(subAgent) =>
|
|
83
|
-
subAgent.silentSeconds >= thresholds.subAgentSilentThresholdSeconds,
|
|
84
|
-
);
|
|
85
|
-
const longRunningSubAgents = subAgents.filter(
|
|
86
|
-
(subAgent) =>
|
|
87
|
-
subAgent.runningSeconds >= thresholds.subAgentRunningThresholdSeconds,
|
|
88
|
-
);
|
|
77
|
+
composeSubAgentSection = (stallSections: SubAgentStallSections): string => {
|
|
89
78
|
const sections: string[] = [];
|
|
90
|
-
if (idleSubAgents.length > 0) {
|
|
91
|
-
sections.push(composeIdleSubAgentSection(idleSubAgents));
|
|
79
|
+
if (stallSections.idleSubAgents.length > 0) {
|
|
80
|
+
sections.push(composeIdleSubAgentSection(stallSections.idleSubAgents));
|
|
92
81
|
}
|
|
93
|
-
if (longRunningSubAgents.length > 0) {
|
|
94
|
-
sections.push(
|
|
82
|
+
if (stallSections.longRunningSubAgents.length > 0) {
|
|
83
|
+
sections.push(
|
|
84
|
+
composeLongRunningSubAgentSection(stallSections.longRunningSubAgents),
|
|
85
|
+
);
|
|
95
86
|
}
|
|
96
87
|
return sections.join('\n\n');
|
|
97
88
|
};
|