@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.78.1",
3
+ "version": "1.78.3",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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 to upload the log file
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,
@@ -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
- // Build command flags
93
- // IMPORTANT: When using command-stream's $ template tag, each ${} interpolation is treated
94
- // as a single argument. DO NOT use commandArgs.join(' ') as it will make all flags part
95
- // of the first positional argument, causing "File does not exist" errors.
96
- // See case study: docs/case-studies/issue-1096/README.md
97
- const publicFlag = isPublic ? '--public' : '--private';
149
+ const commandArgs = buildGhUploadLogArgs({
150
+ logFile,
151
+ isPublic,
152
+ description,
153
+ verbose,
154
+ });
98
155
 
99
156
  if (verbose) {
100
- const descDisplay = description ? ` --description "${description}"` : '';
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
- // Execute command with separate interpolations for each argument
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 the
276
- // linked PR so the completion message can include both an `Issue:` and
277
- // a `Pull request:` line. Failures are logged and ignored — the
278
- // notification still goes out without the PR line.
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, { verbose, lookupLinkedPullRequest: options.lookupLinkedPullRequest });
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
- return await lookupLinkedPullRequest(ctx);
410
- }
411
-
412
- try {
413
- const { batchCheckPullRequestsForIssues } = await import('./github.lib.mjs');
414
- const result = await batchCheckPullRequestsForIssues(ctx.owner, ctx.repo, [ctx.number]);
415
- const linkedPRs = result?.[ctx.number]?.linkedPRs || [];
416
- if (linkedPRs.length > 0 && linkedPRs[0].url) {
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] Found linked PR ${linkedPRs[0].url} for issue ${ctx.owner}/${ctx.repo}#${ctx.number}`);
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