@link-assistant/hive-mind 1.25.8 → 1.26.1

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,26 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.26.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 278415a: fix: post "Ready to merge" comment after auto-restart sequence with --auto-restart-until-mergeable (Issue #1371)
8
+
9
+ When `--auto-restart-until-mergeable` was used after a regular auto-restart sequence (triggered by uncommitted changes), the "Ready to merge" comment was silently suppressed because `checkForExistingComment` found a matching comment from a previous `solve` run.
10
+
11
+ The deduplication logic in `watchUntilMergeable` now uses an in-memory flag (`readyToMergeCommentPosted`) scoped to the current session, rather than searching all PR comment history. This correctly prevents duplicate comments within a single run while allowing new notifications when a fresh `solve` invocation starts.
12
+
13
+ ## 1.26.0
14
+
15
+ ### Minor Changes
16
+
17
+ - d96ae3b: feat: /merge command syncs ready tags between linked PRs and issues (Issue #1367)
18
+
19
+ The `/merge` Telegram bot command now syncs the `ready` label between PRs and their linked issues before building the merge queue.
20
+ - If a PR has the `ready` label and its body links to an issue via standard GitHub closing keywords (fixes/closes/resolves #N), the linked issue also gets the `ready` label
21
+ - If an issue has the `ready` label and has a clearly linked open PR (found via body search), the PR also gets the `ready` label
22
+ - Sync happens during `MergeQueueProcessor.initialize()`, before the final list of ready PRs is collected
23
+
3
24
  ## 1.25.8
4
25
 
5
26
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.25.8",
3
+ "version": "1.26.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -1475,15 +1475,5 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
1475
1475
  }
1476
1476
  };
1477
1477
  // Export all functions as default object too
1478
- export default {
1479
- validateClaudeConnection,
1480
- handleClaudeRuntimeSwitch,
1481
- executeClaude,
1482
- executeClaudeCommand,
1483
- checkForUncommittedChanges,
1484
- calculateSessionTokens,
1485
- getClaudeVersion,
1486
- setClaudeVersion,
1487
- resolveThinkingSettings,
1488
- checkModelVisionCapability,
1489
- };
1478
+ // prettier-ignore
1479
+ export default { validateClaudeConnection, handleClaudeRuntimeSwitch, executeClaude, executeClaudeCommand, checkForUncommittedChanges, calculateSessionTokens, getClaudeVersion, setClaudeVersion, resolveThinkingSettings, checkModelVisionCapability };
@@ -19,6 +19,9 @@ const exec = promisify(execCallback);
19
19
  // Import GitHub URL parser
20
20
  import { parseGitHubUrl } from './github.lib.mjs';
21
21
 
22
+ // Import linking utilities
23
+ import { extractLinkedIssueNumber } from './github-linking.lib.mjs';
24
+
22
25
  // Default label configuration
23
26
  export const READY_LABEL = {
24
27
  name: 'ready',
@@ -251,6 +254,172 @@ export async function fetchReadyIssuesWithPRs(owner, repo, verbose = false) {
251
254
  }
252
255
  }
253
256
 
257
+ /**
258
+ * Add a label to a GitHub issue or pull request
259
+ * @param {'issue'|'pr'} type - Whether to add to issue or PR
260
+ * @param {string} owner - Repository owner
261
+ * @param {string} repo - Repository name
262
+ * @param {number} number - Issue or PR number
263
+ * @param {string} labelName - Label name to add
264
+ * @param {boolean} verbose - Whether to log verbose output
265
+ * @returns {Promise<{success: boolean, error: string|null}>}
266
+ */
267
+ async function addLabel(type, owner, repo, number, labelName, verbose = false) {
268
+ const cmd = type === 'issue' ? 'issue' : 'pr';
269
+ try {
270
+ await exec(`gh ${cmd} edit ${number} --repo ${owner}/${repo} --add-label "${labelName}"`);
271
+ if (verbose) console.log(`[VERBOSE] /merge: Added '${labelName}' label to ${type} #${number}`);
272
+ return { success: true, error: null };
273
+ } catch (error) {
274
+ if (verbose) console.log(`[VERBOSE] /merge: Failed to add label to ${type} #${number}: ${error.message}`);
275
+ return { success: false, error: error.message };
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Sync 'ready' tags between linked pull requests and issues
281
+ *
282
+ * Issue #1367: Before building the merge queue, ensure that:
283
+ * 1. If a PR has 'ready' label and is clearly linked to an issue (via standard GitHub
284
+ * keywords in the PR body/title), the issue also gets 'ready' label.
285
+ * 2. If an issue has 'ready' label and has a clearly linked open PR, the PR also gets
286
+ * 'ready' label.
287
+ *
288
+ * This ensures the final list of ready PRs reflects all ready work, regardless of
289
+ * where the 'ready' label was originally applied.
290
+ *
291
+ * @param {string} owner - Repository owner
292
+ * @param {string} repo - Repository name
293
+ * @param {boolean} verbose - Whether to log verbose output
294
+ * @returns {Promise<{synced: number, errors: number, details: Array<Object>}>}
295
+ */
296
+ export async function syncReadyTags(owner, repo, verbose = false) {
297
+ const synced = [];
298
+ const errors = [];
299
+
300
+ if (verbose) {
301
+ console.log(`[VERBOSE] /merge: Syncing 'ready' tags for ${owner}/${repo}...`);
302
+ }
303
+
304
+ try {
305
+ // Fetch open PRs with 'ready' label (including body for link detection)
306
+ const { stdout: prsJson } = await exec(`gh pr list --repo ${owner}/${repo} --label "${READY_LABEL.name}" --state open --json number,title,body,labels --limit 100`);
307
+ const readyPRs = JSON.parse(prsJson.trim() || '[]');
308
+
309
+ if (verbose) {
310
+ console.log(`[VERBOSE] /merge: Found ${readyPRs.length} open PRs with 'ready' label for tag sync`);
311
+ }
312
+
313
+ // Fetch open issues with 'ready' label
314
+ const { stdout: issuesJson } = await exec(`gh issue list --repo ${owner}/${repo} --label "${READY_LABEL.name}" --state open --json number,title --limit 100`);
315
+ const readyIssues = JSON.parse(issuesJson.trim() || '[]');
316
+
317
+ if (verbose) {
318
+ console.log(`[VERBOSE] /merge: Found ${readyIssues.length} open issues with 'ready' label for tag sync`);
319
+ }
320
+
321
+ // Build a set of issue numbers that already have 'ready'
322
+ const readyIssueNumbers = new Set(readyIssues.map(i => String(i.number)));
323
+
324
+ // Step 1: For each PR with 'ready', find linked issue and sync label to it
325
+ for (const pr of readyPRs) {
326
+ try {
327
+ const prBody = pr.body || '';
328
+ const linkedIssueNumber = extractLinkedIssueNumber(prBody);
329
+
330
+ if (!linkedIssueNumber) {
331
+ if (verbose) {
332
+ console.log(`[VERBOSE] /merge: PR #${pr.number} has no linked issue (no closing keyword in body)`);
333
+ }
334
+ continue;
335
+ }
336
+
337
+ if (readyIssueNumbers.has(String(linkedIssueNumber))) {
338
+ if (verbose) {
339
+ console.log(`[VERBOSE] /merge: Issue #${linkedIssueNumber} already has 'ready' label (linked from PR #${pr.number})`);
340
+ }
341
+ continue;
342
+ }
343
+
344
+ // Issue doesn't have 'ready' label yet - add it
345
+ if (verbose) {
346
+ console.log(`[VERBOSE] /merge: PR #${pr.number} has 'ready', adding to linked issue #${linkedIssueNumber}`);
347
+ }
348
+
349
+ const result = await addLabel('issue', owner, repo, linkedIssueNumber, READY_LABEL.name, verbose);
350
+ if (result.success) {
351
+ synced.push({ type: 'pr-to-issue', prNumber: pr.number, issueNumber: Number(linkedIssueNumber) });
352
+ // Mark this issue as now having 'ready' so we don't process it again
353
+ readyIssueNumbers.add(String(linkedIssueNumber));
354
+ } else {
355
+ errors.push({ type: 'pr-to-issue', prNumber: pr.number, issueNumber: Number(linkedIssueNumber), error: result.error });
356
+ }
357
+ } catch (err) {
358
+ if (verbose) {
359
+ console.log(`[VERBOSE] /merge: Error syncing label from PR #${pr.number}: ${err.message}`);
360
+ }
361
+ errors.push({ type: 'pr-to-issue', prNumber: pr.number, error: err.message });
362
+ }
363
+ }
364
+
365
+ // Build a set of PR numbers that already have 'ready'
366
+ const readyPRNumbers = new Set(readyPRs.map(p => String(p.number)));
367
+
368
+ // Step 2: For each issue with 'ready', find linked PRs and sync label to them
369
+ for (const issue of readyIssues) {
370
+ try {
371
+ // Search for open PRs linked to this issue via closing keywords
372
+ const { stdout: linkedPRsJson } = await exec(`gh pr list --repo ${owner}/${repo} --search "in:body closes #${issue.number} OR fixes #${issue.number} OR resolves #${issue.number}" --state open --json number,title,labels --limit 10`);
373
+ const linkedPRs = JSON.parse(linkedPRsJson.trim() || '[]');
374
+
375
+ for (const linkedPR of linkedPRs) {
376
+ if (readyPRNumbers.has(String(linkedPR.number))) {
377
+ if (verbose) {
378
+ console.log(`[VERBOSE] /merge: PR #${linkedPR.number} already has 'ready' label (linked from issue #${issue.number})`);
379
+ }
380
+ continue;
381
+ }
382
+
383
+ // PR doesn't have 'ready' label yet - add it
384
+ if (verbose) {
385
+ console.log(`[VERBOSE] /merge: Issue #${issue.number} has 'ready', adding to linked PR #${linkedPR.number}`);
386
+ }
387
+
388
+ const result = await addLabel('pr', owner, repo, linkedPR.number, READY_LABEL.name, verbose);
389
+ if (result.success) {
390
+ synced.push({ type: 'issue-to-pr', issueNumber: issue.number, prNumber: linkedPR.number });
391
+ // Mark this PR as now having 'ready'
392
+ readyPRNumbers.add(String(linkedPR.number));
393
+ } else {
394
+ errors.push({ type: 'issue-to-pr', issueNumber: issue.number, prNumber: linkedPR.number, error: result.error });
395
+ }
396
+ }
397
+ } catch (err) {
398
+ if (verbose) {
399
+ console.log(`[VERBOSE] /merge: Error syncing label from issue #${issue.number}: ${err.message}`);
400
+ }
401
+ errors.push({ type: 'issue-to-pr', issueNumber: issue.number, error: err.message });
402
+ }
403
+ }
404
+ } catch (error) {
405
+ if (verbose) {
406
+ console.log(`[VERBOSE] /merge: Error during tag sync: ${error.message}`);
407
+ }
408
+ errors.push({ type: 'fetch', error: error.message });
409
+ }
410
+
411
+ if (verbose) {
412
+ console.log(`[VERBOSE] /merge: Tag sync complete. Synced: ${synced.length}, Errors: ${errors.length}`);
413
+ }
414
+
415
+ return {
416
+ synced: synced.length,
417
+ errors: errors.length,
418
+ details: synced,
419
+ errorDetails: errors,
420
+ };
421
+ }
422
+
254
423
  /**
255
424
  * Get combined list of ready PRs (from both direct PR labels and issue labels)
256
425
  * @param {string} owner - Repository owner
@@ -1283,6 +1452,8 @@ export default {
1283
1452
  fetchReadyPullRequests,
1284
1453
  fetchReadyIssuesWithPRs,
1285
1454
  getAllReadyPRs,
1455
+ // Issue #1367: Sync 'ready' tags between linked PRs and issues
1456
+ syncReadyTags,
1286
1457
  checkPRCIStatus,
1287
1458
  checkPRMergeable,
1288
1459
  checkMergePermissions,
@@ -345,6 +345,13 @@ export const watchUntilMergeable = async params => {
345
345
  // `restartCount` counts actual AI tool executions (when we actually restart the AI)
346
346
  let restartCount = 0;
347
347
 
348
+ // Issue #1371: Track whether a "Ready to merge" comment was posted in THIS session.
349
+ // This replaces the all-time history check (checkForExistingComment) which incorrectly
350
+ // suppressed new notifications when a previous solve run had already posted one.
351
+ // In-memory deduplication correctly handles the case where multiple check cycles in
352
+ // the same run detect mergeability simultaneously, without blocking fresh runs.
353
+ let readyToMergeCommentPosted = false;
354
+
348
355
  let currentBackoffSeconds = watchInterval;
349
356
 
350
357
  await log('');
@@ -430,18 +437,19 @@ export const watchUntilMergeable = async params => {
430
437
  await log(formatAligned('', 'PR is ready to be merged manually', '', 2));
431
438
  await log(formatAligned('', 'Exiting auto-restart-until-mergeable mode', '', 2));
432
439
 
433
- // Issue #1323: Post success comment only if one doesn't already exist
434
- // This prevents duplicate comments when multiple processes reach this point
440
+ // Issue #1371: Post success comment only if not already posted in this session.
441
+ // Use in-memory flag instead of checking all PR comment history (issue #1323),
442
+ // since the historical check incorrectly suppressed notifications when a
443
+ // previous solve run had already posted a "Ready to merge" comment.
435
444
  try {
436
- const readyToMergeSignature = '## ✅ Ready to merge';
437
- const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
438
- if (!hasExistingComment) {
445
+ if (!readyToMergeCommentPosted) {
439
446
  // Issue #1345: Differentiate message when no CI is configured
440
447
  const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : '- All CI checks have passed';
441
448
  const commentBody = `## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
442
449
  await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
450
+ readyToMergeCommentPosted = true;
443
451
  } else {
444
- await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment', '', 2));
452
+ await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment (already posted this session)', '', 2));
445
453
  }
446
454
  } catch {
447
455
  // Don't fail if comment posting fails
@@ -16,7 +16,7 @@
16
16
  * @see https://github.com/link-assistant/hive-mind/issues/1143
17
17
  */
18
18
 
19
- import { getAllReadyPRs, checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI, ensureReadyLabel, waitForBranchCI, getDefaultBranch, waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge.lib.mjs';
19
+ import { getAllReadyPRs, checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI, ensureReadyLabel, waitForBranchCI, getDefaultBranch, waitForCommitCI, checkBranchCIHealth, getMergeCommitSha, syncReadyTags } from './github-merge.lib.mjs';
20
20
  import { mergeQueue as mergeQueueConfig } from './config.lib.mjs';
21
21
  import { getProgressBar } from './limits.lib.mjs';
22
22
 
@@ -197,6 +197,16 @@ export class MergeQueueProcessor {
197
197
  this.log("Created 'ready' label in repository");
198
198
  }
199
199
 
200
+ // Issue #1367: Sync 'ready' tags between linked PRs and issues before collecting the queue
201
+ // This ensures the final list reflects all ready work regardless of where the tag was applied
202
+ const syncResult = await syncReadyTags(this.owner, this.repo, this.verbose);
203
+ if (syncResult.synced > 0) {
204
+ this.log(`Synced 'ready' tag: ${syncResult.synced} item(s) updated`);
205
+ }
206
+ if (syncResult.errors > 0) {
207
+ this.log(`Tag sync had ${syncResult.errors} error(s) (non-fatal, proceeding)`);
208
+ }
209
+
200
210
  // Fetch all ready PRs
201
211
  const readyPRs = await getAllReadyPRs(this.owner, this.repo, this.verbose);
202
212