@link-assistant/hive-mind 1.78.1 → 1.78.3
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 +12 -0
- package/package.json +1 -1
- package/src/github.lib.mjs +1 -1
- package/src/log-upload.lib.mjs +66 -20
- package/src/session-monitor.lib.mjs +82 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.78.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b346808: Use the latest gh-upload-log package for attached log uploads and rely on its default auto mode/shared repository fallback instead of passing explicit strategy flags.
|
|
8
|
+
|
|
9
|
+
## 1.78.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 70db26f: Ensure Telegram work-session completion messages recover pull request links from completed solve logs when linked-issue lookup does not return a PR.
|
|
14
|
+
|
|
3
15
|
## 1.78.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/github.lib.mjs
CHANGED
|
@@ -631,7 +631,7 @@ ${logContent}
|
|
|
631
631
|
// Use the original sanitized content for upload since it's a plain text file
|
|
632
632
|
await fs.writeFile(tempLogFile, await sanitizeLogContent(rawLogContent));
|
|
633
633
|
|
|
634
|
-
// Use gh-upload-log
|
|
634
|
+
// Use gh-upload-log default auto mode and shared repository fallback.
|
|
635
635
|
const uploadDescription = `Solution draft log for https://github.com/${owner}/${repo}/${targetType === 'pr' ? 'pull' : 'issues'}/${targetNumber}`;
|
|
636
636
|
const uploadResult = await uploadLogWithGhUploadLog({
|
|
637
637
|
logFile: tempLogFile,
|
package/src/log-upload.lib.mjs
CHANGED
|
@@ -28,6 +28,63 @@ const summarizeCommandOutput = value => {
|
|
|
28
28
|
return text.length > 500 ? `${text.slice(0, 500)}... [truncated ${text.length - 500} chars]` : text;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
export const buildGhUploadLogArgs = ({ logFile, isPublic, description, verbose = false }) => {
|
|
32
|
+
if (!logFile) {
|
|
33
|
+
throw new Error('logFile is required for gh-upload-log');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const args = [logFile, isPublic ? '--public' : '--private'];
|
|
37
|
+
|
|
38
|
+
if (description) {
|
|
39
|
+
args.push('--description', description);
|
|
40
|
+
}
|
|
41
|
+
if (verbose) {
|
|
42
|
+
args.push('--verbose');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return args;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const quoteShellArg = value => {
|
|
49
|
+
const text = String(value);
|
|
50
|
+
if (/^[A-Za-z0-9_./:=@+-]+$/u.test(text)) return text;
|
|
51
|
+
return `"${text.replace(/(["\\$`])/gu, '\\$1')}"`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatGhUploadLogCommand = args => `gh-upload-log ${args.map(quoteShellArg).join(' ')}`;
|
|
55
|
+
|
|
56
|
+
const runGhUploadLogCommand = async args => {
|
|
57
|
+
const { spawn } = await use('child_process');
|
|
58
|
+
|
|
59
|
+
return new Promise(resolve => {
|
|
60
|
+
const child = spawn('gh-upload-log', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
61
|
+
let stdout = '';
|
|
62
|
+
let stderr = '';
|
|
63
|
+
let settled = false;
|
|
64
|
+
|
|
65
|
+
const settle = value => {
|
|
66
|
+
if (!settled) {
|
|
67
|
+
settled = true;
|
|
68
|
+
resolve(value);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
child.stdout?.on('data', chunk => {
|
|
73
|
+
stdout += chunk.toString();
|
|
74
|
+
});
|
|
75
|
+
child.stderr?.on('data', chunk => {
|
|
76
|
+
stderr += chunk.toString();
|
|
77
|
+
});
|
|
78
|
+
child.on('error', error => {
|
|
79
|
+
const errorText = stderr ? `${stderr}\n${error.message}` : error.message;
|
|
80
|
+
settle({ code: error.code === 'ENOENT' ? 127 : 1, stdout, stderr: errorText });
|
|
81
|
+
});
|
|
82
|
+
child.on('close', code => {
|
|
83
|
+
settle({ code: code ?? 1, stdout, stderr });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
31
88
|
export const parseGhUploadLogOutput = outputValue => {
|
|
32
89
|
const output = outputValue?.toString?.() || '';
|
|
33
90
|
const parsed = {
|
|
@@ -89,30 +146,18 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
89
146
|
const result = { success: false, url: null, rawUrl: null, type: null, chunks: 1 };
|
|
90
147
|
|
|
91
148
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
const commandArgs = buildGhUploadLogArgs({
|
|
150
|
+
logFile,
|
|
151
|
+
isPublic,
|
|
152
|
+
description,
|
|
153
|
+
verbose,
|
|
154
|
+
});
|
|
98
155
|
|
|
99
156
|
if (verbose) {
|
|
100
|
-
|
|
101
|
-
await log(` 📤 Running: gh-upload-log "${logFile}" ${publicFlag}${descDisplay} --verbose`, { verbose: true });
|
|
157
|
+
await log(` 📤 Running: ${formatGhUploadLogCommand(commandArgs)}`, { verbose: true });
|
|
102
158
|
}
|
|
103
159
|
|
|
104
|
-
|
|
105
|
-
// Each ${} is properly passed as a separate argument to the shell
|
|
106
|
-
let uploadResult;
|
|
107
|
-
if (description && verbose) {
|
|
108
|
-
uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --description ${description} --verbose`;
|
|
109
|
-
} else if (description) {
|
|
110
|
-
uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --description ${description}`;
|
|
111
|
-
} else if (verbose) {
|
|
112
|
-
uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --verbose`;
|
|
113
|
-
} else {
|
|
114
|
-
uploadResult = await $`gh-upload-log ${logFile} ${publicFlag}`;
|
|
115
|
-
}
|
|
160
|
+
const uploadResult = await runGhUploadLogCommand(commandArgs);
|
|
116
161
|
const output = (uploadResult.stdout?.toString() || '') + (uploadResult.stderr?.toString() || '');
|
|
117
162
|
|
|
118
163
|
if (uploadResult.code !== 0) {
|
|
@@ -252,5 +297,6 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
252
297
|
// Export all functions as default object too
|
|
253
298
|
export default {
|
|
254
299
|
parseGhUploadLogOutput,
|
|
300
|
+
buildGhUploadLogArgs,
|
|
255
301
|
uploadLogWithGhUploadLog,
|
|
256
302
|
};
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* @see https://github.com/link-assistant/hive-mind/issues/380
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { promisify } from 'util';
|
|
19
18
|
import { exec as execCallback } from 'child_process';
|
|
19
|
+
import fs from 'fs/promises';
|
|
20
|
+
import { promisify } from 'util';
|
|
20
21
|
import { formatSessionCompletionMessage, getSessionCompletionExitCode } from './work-session-formatting.lib.mjs';
|
|
21
22
|
import { notifySubscribers, getSubscriberCount } from './telegram-subscribers.lib.mjs';
|
|
22
23
|
|
|
@@ -157,6 +158,45 @@ function normalizeSessionUrl(url) {
|
|
|
157
158
|
return url.replace(/#.*$/, '').replace(/\/+$/, '').toLowerCase();
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
const GITHUB_PULL_REQUEST_URL_RE = /https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/([0-9]+)/g;
|
|
162
|
+
|
|
163
|
+
export function extractPullRequestUrlFromText(text, { owner = null, repo = null } = {}) {
|
|
164
|
+
if (!text) return null;
|
|
165
|
+
|
|
166
|
+
const expectedOwner = owner ? String(owner).toLowerCase() : null;
|
|
167
|
+
const expectedRepo = repo ? String(repo).toLowerCase() : null;
|
|
168
|
+
const value = String(text);
|
|
169
|
+
GITHUB_PULL_REQUEST_URL_RE.lastIndex = 0;
|
|
170
|
+
|
|
171
|
+
let match;
|
|
172
|
+
while ((match = GITHUB_PULL_REQUEST_URL_RE.exec(value)) !== null) {
|
|
173
|
+
const [, matchOwner, matchRepo, pullNumber] = match;
|
|
174
|
+
if (expectedOwner && matchOwner.toLowerCase() !== expectedOwner) continue;
|
|
175
|
+
if (expectedRepo && matchRepo.toLowerCase() !== expectedRepo) continue;
|
|
176
|
+
return `https://github.com/${matchOwner}/${matchRepo}/pull/${pullNumber}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function resolvePullRequestUrlFromSessionLog(logPath, ctx, { verbose = false, readFile = fs.readFile } = {}) {
|
|
183
|
+
if (!logPath) return null;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const logText = await readFile(logPath, 'utf8');
|
|
187
|
+
const pullRequestUrl = extractPullRequestUrlFromText(logText, { owner: ctx.owner, repo: ctx.repo });
|
|
188
|
+
if (pullRequestUrl && verbose) {
|
|
189
|
+
console.log(`[VERBOSE] Found PR ${pullRequestUrl} in completed session log ${logPath}`);
|
|
190
|
+
}
|
|
191
|
+
return pullRequestUrl;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (verbose) {
|
|
194
|
+
console.log(`[VERBOSE] Could not inspect session log ${logPath} for PR URL: ${error?.message || error}`);
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
160
200
|
function isNonIsolationSessionActive(sessionName, sessionInfo, verbose = false) {
|
|
161
201
|
const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
|
|
162
202
|
const elapsed = Date.now() - startTime.getTime();
|
|
@@ -272,13 +312,19 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
272
312
|
try {
|
|
273
313
|
const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
|
|
274
314
|
|
|
275
|
-
// Issue #1688: When the original /solve URL was an issue, look up
|
|
276
|
-
//
|
|
277
|
-
// a `Pull request:` line.
|
|
278
|
-
//
|
|
315
|
+
// Issue #1688/#1905: When the original /solve URL was an issue, look up
|
|
316
|
+
// the created PR so the completion message can include both an
|
|
317
|
+
// `Issue:` and a `Pull request:` line. The linked-issue API can lag
|
|
318
|
+
// behind the solver's own verification log, so we also inspect the
|
|
319
|
+
// completed session log before giving up.
|
|
279
320
|
let pullRequestUrl = null;
|
|
280
321
|
try {
|
|
281
|
-
pullRequestUrl = await resolvePullRequestUrlForSession(sessionInfo, {
|
|
322
|
+
pullRequestUrl = await resolvePullRequestUrlForSession(sessionInfo, {
|
|
323
|
+
verbose,
|
|
324
|
+
lookupLinkedPullRequest: options.lookupLinkedPullRequest,
|
|
325
|
+
statusResult,
|
|
326
|
+
readFile: options.readFile,
|
|
327
|
+
});
|
|
282
328
|
} catch (lookupError) {
|
|
283
329
|
if (verbose) {
|
|
284
330
|
console.log(`[VERBOSE] Pull request lookup failed for ${sessionName}: ${lookupError?.message || lookupError}`);
|
|
@@ -395,36 +441,50 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
395
441
|
* @param {Object} [options]
|
|
396
442
|
* @param {boolean} [options.verbose]
|
|
397
443
|
* @param {Function} [options.lookupLinkedPullRequest] - Optional override `(ctx) => Promise<string|null>`
|
|
444
|
+
* @param {Object} [options.statusResult] - Completed start-command status payload, including logPath
|
|
445
|
+
* @param {Function} [options.readFile] - Optional test override for reading session logs
|
|
398
446
|
* @returns {Promise<string|null>} PR URL or null
|
|
399
447
|
*
|
|
400
448
|
* @see https://github.com/link-assistant/hive-mind/issues/1688
|
|
449
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1905
|
|
401
450
|
*/
|
|
402
|
-
async function resolvePullRequestUrlForSession(sessionInfo, { verbose = false, lookupLinkedPullRequest = null } = {}) {
|
|
451
|
+
async function resolvePullRequestUrlForSession(sessionInfo, { verbose = false, lookupLinkedPullRequest = null, statusResult = null, readFile = fs.readFile } = {}) {
|
|
403
452
|
const ctx = sessionInfo?.urlContext;
|
|
404
453
|
if (!ctx || ctx.type !== 'issue' || !ctx.owner || !ctx.repo || !ctx.number) {
|
|
405
454
|
return null;
|
|
406
455
|
}
|
|
407
456
|
|
|
408
457
|
if (typeof lookupLinkedPullRequest === 'function') {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
458
|
+
const linkedPullRequestUrl = await lookupLinkedPullRequest(ctx);
|
|
459
|
+
if (linkedPullRequestUrl) return linkedPullRequestUrl;
|
|
460
|
+
} else {
|
|
461
|
+
try {
|
|
462
|
+
const { batchCheckPullRequestsForIssues } = await import('./github.lib.mjs');
|
|
463
|
+
const result = await batchCheckPullRequestsForIssues(ctx.owner, ctx.repo, [ctx.number]);
|
|
464
|
+
const linkedPRs = result?.[ctx.number]?.linkedPRs || [];
|
|
465
|
+
if (linkedPRs.length > 0 && linkedPRs[0].url) {
|
|
466
|
+
if (verbose) {
|
|
467
|
+
console.log(`[VERBOSE] Found linked PR ${linkedPRs[0].url} for issue ${ctx.owner}/${ctx.repo}#${ctx.number}`);
|
|
468
|
+
}
|
|
469
|
+
return linkedPRs[0].url;
|
|
470
|
+
}
|
|
471
|
+
} catch (error) {
|
|
417
472
|
if (verbose) {
|
|
418
|
-
console.log(`[VERBOSE]
|
|
473
|
+
console.log(`[VERBOSE] batchCheckPullRequestsForIssues failed for ${ctx.owner}/${ctx.repo}#${ctx.number}: ${error?.message || error}`);
|
|
419
474
|
}
|
|
420
|
-
return linkedPRs[0].url;
|
|
421
|
-
}
|
|
422
|
-
} catch (error) {
|
|
423
|
-
if (verbose) {
|
|
424
|
-
console.log(`[VERBOSE] batchCheckPullRequestsForIssues failed for ${ctx.owner}/${ctx.repo}#${ctx.number}: ${error?.message || error}`);
|
|
425
475
|
}
|
|
426
|
-
throw error;
|
|
427
476
|
}
|
|
477
|
+
|
|
478
|
+
const logPath = statusResult?.logPath || sessionInfo?.logPath || null;
|
|
479
|
+
const pullRequestUrlFromLog = await resolvePullRequestUrlFromSessionLog(logPath, ctx, { verbose, readFile });
|
|
480
|
+
if (pullRequestUrlFromLog) return pullRequestUrlFromLog;
|
|
481
|
+
|
|
482
|
+
if (verbose && logPath) {
|
|
483
|
+
console.log(`[VERBOSE] No PR URL found for issue ${ctx.owner}/${ctx.repo}#${ctx.number} in session log ${logPath}`);
|
|
484
|
+
} else if (verbose) {
|
|
485
|
+
console.log(`[VERBOSE] No session log path available for PR URL fallback for issue ${ctx.owner}/${ctx.repo}#${ctx.number}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
428
488
|
return null;
|
|
429
489
|
}
|
|
430
490
|
|