github-issue-tower-defence-management 1.148.11 → 1.148.13

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 (28) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +6 -0
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  4. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +3 -3
  5. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  6. package/bin/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.js +64 -0
  7. package/bin/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.js.map +1 -0
  8. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +71 -3
  9. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
  10. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +22 -3
  11. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
  14. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +10 -2
  15. package/src/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.test.ts +55 -0
  16. package/src/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.ts +36 -0
  17. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.queuedReply.test.ts +171 -0
  18. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +90 -3
  19. package/src/domain/usecases/CreateNewStoryByLabelUseCase.test.ts +216 -0
  20. package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +44 -6
  21. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  22. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +1 -0
  23. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  24. package/types/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.d.ts +2 -0
  25. package/types/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.d.ts.map +1 -0
  26. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +3 -1
  27. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
  28. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
@@ -0,0 +1,171 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { TranscriptOwnerCallStatusProvider } from './TranscriptOwnerCallStatusProvider';
5
+ import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSessionReminderSentinel';
6
+
7
+ describe('TranscriptOwnerCallStatusProvider owner reply typed mid-turn', () => {
8
+ let rootDirectory: string;
9
+ let markerDirectory: string;
10
+
11
+ beforeEach(() => {
12
+ rootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'queued-reply-'));
13
+ markerDirectory = path.join(rootDirectory, 'markers');
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(rootDirectory, { force: true, recursive: true });
18
+ });
19
+
20
+ const sessionId = 'e7a58bf9-4d13-4e27-b446-52c3b017ad79';
21
+ const callTimestamp = '2026-08-10T11:04:26.331Z';
22
+ const earlierReplyTimestamp = '2026-08-10T11:02:40.286Z';
23
+ const laterReplyTimestamp = '2026-08-10T11:05:53.576Z';
24
+
25
+ const writeTranscript = (lines: object[]): string => {
26
+ const filePath = path.join(rootDirectory, `${sessionId}.jsonl`);
27
+ fs.writeFileSync(
28
+ filePath,
29
+ lines.map((line) => JSON.stringify(line)).join('\n'),
30
+ 'utf8',
31
+ );
32
+ return filePath;
33
+ };
34
+
35
+ const writeReplyMarker = (timestamp: string): void => {
36
+ fs.mkdirSync(markerDirectory, { recursive: true });
37
+ fs.writeFileSync(
38
+ path.join(markerDirectory, `${sessionId}.reply_ts`),
39
+ `${timestamp}\n`,
40
+ 'utf8',
41
+ );
42
+ };
43
+
44
+ const ownerCall = (timestamp: string): object => ({
45
+ type: 'assistant',
46
+ timestamp,
47
+ message: {
48
+ role: 'assistant',
49
+ content: [{ type: 'text', text: 'Please decide <<OWNER_CALL>>' }],
50
+ },
51
+ });
52
+
53
+ const typedReply = (timestamp: string, text: string): object => ({
54
+ type: 'user',
55
+ timestamp,
56
+ promptSource: 'typed',
57
+ origin: { kind: 'human' },
58
+ message: { role: 'user', content: text },
59
+ });
60
+
61
+ const enqueued = (timestamp: string, content: string): object => ({
62
+ type: 'queue-operation',
63
+ operation: 'enqueue',
64
+ timestamp,
65
+ sessionId,
66
+ content,
67
+ });
68
+
69
+ const removed = (timestamp: string, content: string): object => ({
70
+ type: 'queue-operation',
71
+ operation: 'remove',
72
+ timestamp,
73
+ sessionId,
74
+ content,
75
+ });
76
+
77
+ const unansweredSecondsOf = async (
78
+ transcriptPath: string,
79
+ replyMarkerDirectory: string | null,
80
+ ): Promise<number | undefined> => {
81
+ const provider = new TranscriptOwnerCallStatusProvider(
82
+ '<<OWNER_CALL>>',
83
+ replyMarkerDirectory,
84
+ );
85
+ const result =
86
+ await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
87
+ new Map([['session', transcriptPath]]),
88
+ );
89
+ return result.get('session');
90
+ };
91
+
92
+ it('treats a reply enqueued while the agent was working as an answer to the call', async () => {
93
+ const transcriptPath = writeTranscript([
94
+ typedReply(earlierReplyTimestamp, 'an earlier answer'),
95
+ ownerCall(callTimestamp),
96
+ enqueued(laterReplyTimestamp, 'CI が通っていないのに許可はできません'),
97
+ removed(
98
+ '2026-08-10T11:06:07.885Z',
99
+ 'CI が通っていないのに許可はできません',
100
+ ),
101
+ ]);
102
+
103
+ expect(await unansweredSecondsOf(transcriptPath, null)).toBeUndefined();
104
+ });
105
+
106
+ it('keeps the call unanswered when the only later enqueue is a task notification', async () => {
107
+ const transcriptPath = writeTranscript([
108
+ typedReply(earlierReplyTimestamp, 'an earlier answer'),
109
+ ownerCall(callTimestamp),
110
+ enqueued(
111
+ laterReplyTimestamp,
112
+ '<task-notification><task-id>abc</task-id></task-notification>',
113
+ ),
114
+ ]);
115
+
116
+ expect(await unansweredSecondsOf(transcriptPath, null)).toBe(
117
+ Math.floor(Date.parse(callTimestamp) / 1000),
118
+ );
119
+ });
120
+
121
+ it('keeps the call unanswered when the only later enqueue is an injected reminder', async () => {
122
+ const transcriptPath = writeTranscript([
123
+ typedReply(earlierReplyTimestamp, 'an earlier answer'),
124
+ ownerCall(callTimestamp),
125
+ enqueued(
126
+ laterReplyTimestamp,
127
+ `${SILENT_SESSION_REMINDER_SENTINEL} check yourself`,
128
+ ),
129
+ ]);
130
+
131
+ expect(await unansweredSecondsOf(transcriptPath, null)).toBe(
132
+ Math.floor(Date.parse(callTimestamp) / 1000),
133
+ );
134
+ });
135
+
136
+ it('treats the reply time recorded by the status line marker as an answer to the call', async () => {
137
+ const transcriptPath = writeTranscript([
138
+ typedReply(earlierReplyTimestamp, 'an earlier answer'),
139
+ ownerCall(callTimestamp),
140
+ ]);
141
+ writeReplyMarker(laterReplyTimestamp);
142
+
143
+ expect(
144
+ await unansweredSecondsOf(transcriptPath, markerDirectory),
145
+ ).toBeUndefined();
146
+ });
147
+
148
+ it('keeps the call unanswered when the status line marker predates the call', async () => {
149
+ const transcriptPath = writeTranscript([
150
+ typedReply(earlierReplyTimestamp, 'an earlier answer'),
151
+ ownerCall(callTimestamp),
152
+ ]);
153
+ writeReplyMarker(earlierReplyTimestamp);
154
+
155
+ expect(await unansweredSecondsOf(transcriptPath, markerDirectory)).toBe(
156
+ Math.floor(Date.parse(callTimestamp) / 1000),
157
+ );
158
+ });
159
+
160
+ it('keeps the call unanswered when no marker file exists for the session', async () => {
161
+ const transcriptPath = writeTranscript([
162
+ typedReply(earlierReplyTimestamp, 'an earlier answer'),
163
+ ownerCall(callTimestamp),
164
+ ]);
165
+ fs.mkdirSync(markerDirectory, { recursive: true });
166
+
167
+ expect(await unansweredSecondsOf(transcriptPath, markerDirectory)).toBe(
168
+ Math.floor(Date.parse(callTimestamp) / 1000),
169
+ );
170
+ });
171
+ });
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'fs';
2
+ import * as path from 'path';
2
3
  import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
3
4
  import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSessionReminderSentinel';
4
5
 
@@ -100,10 +101,49 @@ export const ownerCallMarkerFamilyResolve = (marker: string): string[] =>
100
101
  ]
101
102
  : [marker];
102
103
 
104
+ // A reply the owner types while the agent is still working is NOT written as a `user` entry: the
105
+ // running turn consumes it from the queue, and the transcript keeps only
106
+ // {"type":"queue-operation","operation":"enqueue","content":"<the text>"} plus its `remove` twin.
107
+ // Reading `user` entries alone therefore misses every mid-turn reply, leaves the call outstanding
108
+ // for the rest of the session, and suppresses the stall reminder of a session the owner has
109
+ // already answered. The exclusions match the ones the status line applies to the same entries, so
110
+ // a system-injected enqueue never counts as an owner reply.
111
+ const INJECTED_ENQUEUE_CONTENT_MARKERS = [
112
+ SILENT_SESSION_REMINDER_SENTINEL,
113
+ '<system-reminder>',
114
+ 'UserPromptSubmit hook',
115
+ '<task-notification>',
116
+ 'SYSTEM NOTIFICATION',
117
+ '<local-command-stdout>',
118
+ '<local-command-caveat>',
119
+ '<command-name>',
120
+ 'This session is being continued from a previous conversation',
121
+ ];
122
+
123
+ const isOwnerEnqueuedReply = (parsed: Record<string, unknown>): boolean => {
124
+ if (readString(parsed, 'operation') !== 'enqueue') {
125
+ return false;
126
+ }
127
+ const content = readString(parsed, 'content');
128
+ if (content === null || content.length === 0) {
129
+ return false;
130
+ }
131
+ return !INJECTED_ENQUEUE_CONTENT_MARKERS.some((injectedMarker) =>
132
+ content.includes(injectedMarker),
133
+ );
134
+ };
135
+
136
+ const TRANSCRIPT_FILE_EXTENSION = '.jsonl';
137
+ const OWNER_REPLY_MARKER_FILE_EXTENSION = '.reply_ts';
138
+ const UNSAFE_SESSION_ID_CHARACTER_PATTERN = /[^A-Za-z0-9._-]/g;
139
+
103
140
  export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
104
141
  private readonly ownerCallMarkerFamily: string[];
105
142
 
106
- constructor(ownerCallMarker: string | null) {
143
+ constructor(
144
+ ownerCallMarker: string | null,
145
+ private readonly ownerReplyMarkerDirectory: string | null = null,
146
+ ) {
107
147
  this.ownerCallMarkerFamily =
108
148
  ownerCallMarker === null || ownerCallMarker.length === 0
109
149
  ? []
@@ -181,13 +221,60 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
181
221
  ) {
182
222
  lastOwnerReplyEpochMs = epochMs;
183
223
  }
224
+ if (type === 'queue-operation' && isOwnerEnqueuedReply(parsed)) {
225
+ lastOwnerReplyEpochMs =
226
+ lastOwnerReplyEpochMs === null || epochMs > lastOwnerReplyEpochMs
227
+ ? epochMs
228
+ : lastOwnerReplyEpochMs;
229
+ }
184
230
  }
185
231
  if (lastOwnerCallEpochMs === null) {
186
232
  return null;
187
233
  }
188
- return lastOwnerReplyEpochMs === null ||
189
- lastOwnerCallEpochMs > lastOwnerReplyEpochMs
234
+ const markerReplyEpochMs = this.readOwnerReplyMarkerEpochMs(transcriptPath);
235
+ const resolvedReplyEpochMs =
236
+ markerReplyEpochMs !== null &&
237
+ (lastOwnerReplyEpochMs === null ||
238
+ markerReplyEpochMs > lastOwnerReplyEpochMs)
239
+ ? markerReplyEpochMs
240
+ : lastOwnerReplyEpochMs;
241
+ return resolvedReplyEpochMs === null ||
242
+ lastOwnerCallEpochMs > resolvedReplyEpochMs
190
243
  ? lastOwnerCallEpochMs
191
244
  : null;
192
245
  };
246
+
247
+ // The owner sees only the status line, so the reply time it renders is the value the owner
248
+ // believes the fleet is acting on. The status line writes that value to a per-session marker
249
+ // file, and reading it here keeps this decision and the owner's own view of it from diverging.
250
+ // The transcript-derived value still stands on its own: an absent, unreadable, or older marker
251
+ // changes nothing, so a fresh host with no markers yet behaves exactly as before.
252
+ private readOwnerReplyMarkerEpochMs = (
253
+ transcriptPath: string,
254
+ ): number | null => {
255
+ if (this.ownerReplyMarkerDirectory === null) {
256
+ return null;
257
+ }
258
+ const fileName = path.basename(transcriptPath);
259
+ const sessionId = fileName.endsWith(TRANSCRIPT_FILE_EXTENSION)
260
+ ? fileName.slice(0, -TRANSCRIPT_FILE_EXTENSION.length)
261
+ : fileName;
262
+ const safeSessionId = sessionId.replace(
263
+ UNSAFE_SESSION_ID_CHARACTER_PATTERN,
264
+ '_',
265
+ );
266
+ let markerContent: string;
267
+ try {
268
+ markerContent = fs.readFileSync(
269
+ path.join(
270
+ this.ownerReplyMarkerDirectory,
271
+ `${safeSessionId}${OWNER_REPLY_MARKER_FILE_EXTENSION}`,
272
+ ),
273
+ 'utf8',
274
+ );
275
+ } catch {
276
+ return null;
277
+ }
278
+ return parseEpochMilliseconds(markerContent.split('\n')[0].trim());
279
+ };
193
280
  }
@@ -96,6 +96,222 @@ describe('CreateNewStoryByLabelUseCase', () => {
96
96
  );
97
97
  });
98
98
 
99
+ describe('logging', () => {
100
+ let logSpy: jest.SpyInstance;
101
+ const lines: string[] = [];
102
+
103
+ beforeEach(() => {
104
+ lines.length = 0;
105
+ logSpy = jest
106
+ .spyOn(console, 'log')
107
+ .mockImplementation((...args: unknown[]) => {
108
+ lines.push(args.map((arg) => String(arg)).join(' '));
109
+ });
110
+ });
111
+
112
+ afterEach(() => {
113
+ logSpy.mockRestore();
114
+ });
115
+
116
+ const loggedLines = (): string[] => lines;
117
+
118
+ it('should record that the option list was left unchanged because every labelled issue title already names a story option', async () => {
119
+ const projectWithExistingOption: Project = {
120
+ ...basicProject,
121
+ story: {
122
+ name: 'Story Field',
123
+ fieldId: 'storyFieldId',
124
+ databaseId: 123,
125
+ stories: [
126
+ {
127
+ id: 'existingNewStoryId',
128
+ name: 'New Feature Request',
129
+ color: 'RED',
130
+ description: '',
131
+ },
132
+ ],
133
+ workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
134
+ },
135
+ };
136
+
137
+ await useCase.run({
138
+ project: projectWithExistingOption,
139
+ cacheUsed: false,
140
+ org: 'testOrg',
141
+ repo: 'testRepo',
142
+ storyObjectMap: new Map([
143
+ [
144
+ 'Story 1',
145
+ {
146
+ story: mock<StoryOption>(),
147
+ storyIssue: mock<Issue>(),
148
+ issues: [issueWithNewStoryLabel],
149
+ },
150
+ ],
151
+ ]),
152
+ issues: [],
153
+ });
154
+
155
+ expect(mockProjectRepository.updateStoryList).not.toHaveBeenCalled();
156
+ expect(
157
+ loggedLines().some((line) =>
158
+ line.includes(
159
+ 'every labelled issue title already names a story option',
160
+ ),
161
+ ),
162
+ ).toBe(true);
163
+ });
164
+
165
+ it('should record that the run found no labelled issue so that the absence of work is distinguishable from the step never running', async () => {
166
+ const storyObjectWithoutNewStoryIssues: StoryObject = {
167
+ story: mock<StoryOption>(),
168
+ storyIssue: mock<Issue>(),
169
+ issues: [regularIssue],
170
+ };
171
+
172
+ await useCase.run({
173
+ project: basicProject,
174
+ cacheUsed: false,
175
+ org: 'testOrg',
176
+ repo: 'testRepo',
177
+ storyObjectMap: new Map([
178
+ ['Story 1', storyObjectWithoutNewStoryIssues],
179
+ ]),
180
+ issues: [regularIssue],
181
+ });
182
+
183
+ expect(
184
+ loggedLines().some((line) =>
185
+ line.includes('found 0 issues carrying the new story label'),
186
+ ),
187
+ ).toBe(true);
188
+ });
189
+
190
+ it('should record the project having no story field', async () => {
191
+ await useCase.run({
192
+ project: { ...basicProject, story: null },
193
+ cacheUsed: false,
194
+ org: 'testOrg',
195
+ repo: 'testRepo',
196
+ storyObjectMap: new Map([['Story 1', storyObjectWithNewStoryIssues]]),
197
+ issues: [],
198
+ });
199
+
200
+ expect(
201
+ loggedLines().some((line) =>
202
+ line.includes('the project has no story field'),
203
+ ),
204
+ ).toBe(true);
205
+ });
206
+
207
+ it('should record the labelled issues, the submitted options, the story link and the label removal', async () => {
208
+ mockProjectRepository.updateStoryList.mockResolvedValue([
209
+ { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
210
+ {
211
+ id: 'newStoryId1',
212
+ name: 'New Feature Request',
213
+ color: 'RED',
214
+ description: '',
215
+ },
216
+ ]);
217
+ const storyObject: StoryObject = {
218
+ story: {
219
+ ...mock<StoryOption>(),
220
+ id: 'story1',
221
+ name: 'Existing Story',
222
+ color: 'BLUE',
223
+ },
224
+ storyIssue: mock<Issue>(),
225
+ issues: [issueWithNewStoryLabel],
226
+ };
227
+
228
+ await useCase.run({
229
+ project: basicProject,
230
+ cacheUsed: false,
231
+ org: 'testOrg',
232
+ repo: 'testRepo',
233
+ storyObjectMap: new Map([['Story 1', storyObject]]),
234
+ issues: [],
235
+ });
236
+
237
+ const lines = loggedLines();
238
+ expect(
239
+ lines.some((line) =>
240
+ line.includes('found 1 issues carrying the new story label'),
241
+ ),
242
+ ).toBe(true);
243
+ expect(
244
+ lines.some(
245
+ (line) =>
246
+ line.includes('labelled issues') &&
247
+ line.includes(issueWithNewStoryLabel.url),
248
+ ),
249
+ ).toBe(true);
250
+ expect(
251
+ lines.some(
252
+ (line) =>
253
+ line.includes('submitting 1 new story options') &&
254
+ line.includes('New Feature Request'),
255
+ ),
256
+ ).toBe(true);
257
+ expect(
258
+ lines.some(
259
+ (line) =>
260
+ line.includes('linked') &&
261
+ line.includes(issueWithNewStoryLabel.url) &&
262
+ line.includes('newStoryId1'),
263
+ ),
264
+ ).toBe(true);
265
+ expect(
266
+ lines.some(
267
+ (line) =>
268
+ line.includes('removed the new story label') &&
269
+ line.includes(issueWithNewStoryLabel.url),
270
+ ),
271
+ ).toBe(true);
272
+ });
273
+
274
+ it('should record that an issue kept its label because no story option matches its title', async () => {
275
+ const issueWithUnmatchedTitle: Issue = {
276
+ ...mock<Issue>(),
277
+ url: 'https://github.com/org/repo/issues/1401',
278
+ title: 'Unmatched Title',
279
+ number: 1401,
280
+ story: 'Existing Story',
281
+ labels: ['new-story'],
282
+ };
283
+ mockProjectRepository.updateStoryList.mockResolvedValue([
284
+ { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
285
+ ]);
286
+
287
+ await useCase.run({
288
+ project: basicProject,
289
+ cacheUsed: false,
290
+ org: 'testOrg',
291
+ repo: 'testRepo',
292
+ storyObjectMap: new Map([
293
+ [
294
+ 'Story 1',
295
+ {
296
+ story: mock<StoryOption>(),
297
+ storyIssue: mock<Issue>(),
298
+ issues: [issueWithUnmatchedTitle],
299
+ },
300
+ ],
301
+ ]),
302
+ issues: [],
303
+ });
304
+
305
+ expect(
306
+ loggedLines().some(
307
+ (line) =>
308
+ line.includes('no story option matches the title') &&
309
+ line.includes(issueWithUnmatchedTitle.url),
310
+ ),
311
+ ).toBe(true);
312
+ });
313
+ });
314
+
99
315
  describe('run', () => {
100
316
  it('should not process anything when project has no story field', async () => {
101
317
  const projectWithoutStory: Project = {
@@ -4,6 +4,8 @@ import { StoryObjectMap } from '../entities/StoryObjectMap';
4
4
  import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
5
5
  import { Issue } from '../entities/Issue';
6
6
 
7
+ const LOG_PREFIX = '[CreateNewStoryByLabel]';
8
+
7
9
  export class CreateNewStoryByLabelUseCase {
8
10
  constructor(
9
11
  readonly projectRepository: Pick<ProjectRepository, 'updateStoryList'>,
@@ -23,30 +25,60 @@ export class CreateNewStoryByLabelUseCase {
23
25
  }): Promise<void> => {
24
26
  const projectStory = input.project.story;
25
27
  if (!projectStory) {
28
+ console.log(
29
+ `${LOG_PREFIX} the project has no story field, so no labelled issue is evaluated. project=${input.project.url}`,
30
+ );
26
31
  return;
27
32
  }
28
33
  const newStoryIssues = this.findNewStoryIssues(
29
34
  input.storyObjectMap,
30
35
  input.issues,
31
36
  );
37
+ console.log(
38
+ `${LOG_PREFIX} found ${newStoryIssues.length} issues carrying the new story label. project=${input.project.url}`,
39
+ );
32
40
  if (newStoryIssues.length === 0) {
33
41
  return;
34
42
  }
43
+ console.log(
44
+ `${LOG_PREFIX} labelled issues: ${newStoryIssues
45
+ .map((issue) => issue.url)
46
+ .join(', ')}`,
47
+ );
35
48
  const newStoryList = this.createNewStoryList(
36
49
  projectStory,
37
50
  input.storyObjectMap,
38
51
  input.issues,
39
52
  );
40
- const savedNewStoryList = newStoryList.some((story) => story.id === null)
41
- ? await this.projectRepository.updateStoryList(
42
- input.project,
43
- newStoryList,
44
- )
45
- : projectStory.stories;
53
+ const addedStories = newStoryList.filter((story) => story.id === null);
54
+ if (addedStories.length === 0) {
55
+ console.log(
56
+ `${LOG_PREFIX} every labelled issue title already names a story option, so the option list is left unchanged`,
57
+ );
58
+ } else {
59
+ console.log(
60
+ `${LOG_PREFIX} submitting ${addedStories.length} new story options: ${addedStories
61
+ .map((story) => story.name)
62
+ .join(', ')}`,
63
+ );
64
+ }
65
+ const savedNewStoryList =
66
+ addedStories.length === 0
67
+ ? projectStory.stories
68
+ : await this.projectRepository.updateStoryList(
69
+ input.project,
70
+ newStoryList,
71
+ );
72
+ console.log(
73
+ `${LOG_PREFIX} the story option list holds ${savedNewStoryList.length} options`,
74
+ );
46
75
 
47
76
  for (const issue of newStoryIssues) {
48
77
  const linkedStory = savedNewStoryList.find((s) => s.name === issue.title);
49
78
  if (!linkedStory) {
79
+ console.log(
80
+ `${LOG_PREFIX} no story option matches the title of ${issue.url}, so it keeps the new story label`,
81
+ );
50
82
  continue;
51
83
  }
52
84
  await this.issueRepository.updateStory(
@@ -54,12 +86,18 @@ export class CreateNewStoryByLabelUseCase {
54
86
  issue,
55
87
  linkedStory.id,
56
88
  );
89
+ console.log(
90
+ `${LOG_PREFIX} linked ${issue.url} to the story option ${linkedStory.id}`,
91
+ );
57
92
  await this.issueRepository.updateLabels(
58
93
  issue,
59
94
  issue.labels.filter(
60
95
  (label) => label.toLowerCase().replace('-', '') !== 'newstory',
61
96
  ),
62
97
  );
98
+ console.log(
99
+ `${LOG_PREFIX} removed the new story label from ${issue.url}`,
100
+ );
63
101
  }
64
102
  };
65
103
 
@@ -1 +1 @@
1
- {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AA2CA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAoD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,EACjB,6BAA4B,MAAM,EAAE,GAAG,IAAW,KACjD,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CA0vBP;CACH"}
1
+ {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AA4CA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAoD3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,EACjB,6BAA4B,MAAM,EAAE,GAAG,IAAW,KACjD,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAowBP;CACH"}
@@ -7,6 +7,7 @@ export type NotifySilentTmuxSessionsParams = {
7
7
  localCommandRunner: LocalCommandRunner;
8
8
  processEnvironReader?: ProcessEnvironReader;
9
9
  ownerCallMarker: string | null;
10
+ ownerReplyMarkerDirectory?: string | null;
10
11
  subAgentOutputRootDirectory: string | null;
11
12
  subAgentProcessMatchPattern: string | null;
12
13
  subAgentTranscriptRootDirectory: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"notifySilentTmuxSessions.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAGpG,OAAO,EAAE,oBAAoB,EAAE,MAAM,kEAAkE,CAAC;AACxG,OAAO,EAEL,qBAAqB,EAQtB,MAAM,0DAA0D,CAAC;AAoBlE,OAAO,EAEL,6BAA6B,EAC9B,MAAM,6DAA6D,CAAC;AAKrE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,0BAA0B,EAAE,MAAM,CAAC;IACnC,+BAA+B,EAAE,MAAM,CAAC;IACxC,8BAA8B,EAAE,MAAM,CAAC;IACvC,+BAA+B,EAAE,MAAM,CAAC;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC,EAAE,MAAM,CAAC;IAC9C,8BAA8B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,CAAC;IACrC,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAuCF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CAoFd,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;;;CAS7C,CAAC"}
1
+ {"version":3,"file":"notifySilentTmuxSessions.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAGpG,OAAO,EAAE,oBAAoB,EAAE,MAAM,kEAAkE,CAAC;AACxG,OAAO,EAEL,qBAAqB,EAQtB,MAAM,0DAA0D,CAAC;AAoBlE,OAAO,EAEL,6BAA6B,EAC9B,MAAM,6DAA6D,CAAC;AAKrE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yBAAyB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,0BAA0B,EAAE,MAAM,CAAC;IACnC,+BAA+B,EAAE,MAAM,CAAC;IACxC,8BAA8B,EAAE,MAAM,CAAC;IACvC,+BAA+B,EAAE,MAAM,CAAC;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC,EAAE,MAAM,CAAC;IAC9C,8BAA8B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,4BAA4B,EAAE,MAAM,CAAC;IACrC,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AA2CF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,8BAA8B,KACrC,OAAO,CAAC,IAAI,CAuFd,CAAC;AAEF,eAAO,MAAM,0CAA0C;;;;;;;;CAS7C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const ownerReplyMarkerDirectoryResolve: (configuredDirectory: string | null, environment: NodeJS.ProcessEnv, userId: number | null) => string | null;
2
+ //# sourceMappingURL=ownerReplyMarkerDirectoryResolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownerReplyMarkerDirectoryResolve.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,gCAAgC,GAC3C,qBAAqB,MAAM,GAAG,IAAI,EAClC,aAAa,MAAM,CAAC,UAAU,EAC9B,QAAQ,MAAM,GAAG,IAAI,KACpB,MAAM,GAAG,IAoBX,CAAC"}
@@ -1,9 +1,11 @@
1
1
  import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
2
2
  export declare const ownerCallMarkerFamilyResolve: (marker: string) => string[];
3
3
  export declare class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
4
+ private readonly ownerReplyMarkerDirectory;
4
5
  private readonly ownerCallMarkerFamily;
5
- constructor(ownerCallMarker: string | null);
6
+ constructor(ownerCallMarker: string | null, ownerReplyMarkerDirectory?: string | null);
6
7
  listUnansweredOwnerCallEpochSecondsBySessionName: (transcriptPathBySessionName: Map<string, string>) => Promise<Map<string, number>>;
7
8
  private findUnansweredOwnerCallEpochMs;
9
+ private readOwnerReplyMarkerEpochMs;
8
10
  }
9
11
  //# sourceMappingURL=TranscriptOwnerCallStatusProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TranscriptOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AA6F3G,eAAO,MAAM,4BAA4B,GAAI,QAAQ,MAAM,KAAG,MAAM,EAMtD,CAAC;AAEf,qBAAa,iCAAkC,YAAW,uBAAuB;IAC/E,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAW;gBAErC,eAAe,EAAE,MAAM,GAAG,IAAI;IAO1C,gDAAgD,GAC9C,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqB7B;IAEF,OAAO,CAAC,8BAA8B,CAsDpC;CACH"}
1
+ {"version":3,"file":"TranscriptOwnerCallStatusProvider.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kEAAkE,CAAC;AA6F3G,eAAO,MAAM,4BAA4B,GAAI,QAAQ,MAAM,KAAG,MAAM,EAMtD,CAAC;AAsCf,qBAAa,iCAAkC,YAAW,uBAAuB;IAK7E,OAAO,CAAC,QAAQ,CAAC,yBAAyB;IAJ5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAW;gBAG/C,eAAe,EAAE,MAAM,GAAG,IAAI,EACb,yBAAyB,GAAE,MAAM,GAAG,IAAW;IAQlE,gDAAgD,GAC9C,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqB7B;IAEF,OAAO,CAAC,8BAA8B,CAmEpC;IAOF,OAAO,CAAC,2BAA2B,CA2BjC;CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreateNewStoryByLabelUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/CreateNewStoryByLabelUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,qBAAa,4BAA4B;IAErC,QAAQ,CAAC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IACtE,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,cAAc,GAAG,aAAa,CAC/B;gBAJQ,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,EAC7D,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,cAAc,GAAG,aAAa,CAC/B;IAGH,GAAG,GAAU,OAAO;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,EAAE,OAAO,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,cAAc,EAAE,cAAc,CAAC;QAC/B,MAAM,EAAE,KAAK,EAAE,CAAC;KACjB,KAAG,OAAO,CAAC,IAAI,CAAC,CAyCf;IAEF,gBAAgB,GAAI,OAAO,KAAK,KAAG,OAAO,CAG7B;IAEb,kBAAkB,GAChB,gBAAgB,cAAc,EAC9B,QAAQ,KAAK,EAAE,KACd,KAAK,EAAE,CAeR;IAEF,kBAAkB,GAChB,cAAc,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAC3C,gBAAgB,cAAc,EAC9B,QAAQ,KAAK,EAAE,KACd,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QAAE,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC,EAAE,CA8B/D;CACH"}
1
+ {"version":3,"file":"CreateNewStoryByLabelUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/CreateNewStoryByLabelUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAI1C,qBAAa,4BAA4B;IAErC,QAAQ,CAAC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IACtE,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,cAAc,GAAG,aAAa,CAC/B;gBAJQ,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,EAC7D,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,cAAc,GAAG,aAAa,CAC/B;IAGH,GAAG,GAAU,OAAO;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,EAAE,OAAO,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,cAAc,EAAE,cAAc,CAAC;QAC/B,MAAM,EAAE,KAAK,EAAE,CAAC;KACjB,KAAG,OAAO,CAAC,IAAI,CAAC,CA6Ef;IAEF,gBAAgB,GAAI,OAAO,KAAK,KAAG,OAAO,CAG7B;IAEb,kBAAkB,GAChB,gBAAgB,cAAc,EAC9B,QAAQ,KAAK,EAAE,KACd,KAAK,EAAE,CAeR;IAEF,kBAAkB,GAChB,cAAc,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAC3C,gBAAgB,cAAc,EAC9B,QAAQ,KAAK,EAAE,KACd,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QAAE,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC,EAAE,CA8B/D;CACH"}