@link-assistant/hive-mind 1.25.8 → 1.26.0

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,16 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d96ae3b: feat: /merge command syncs ready tags between linked PRs and issues (Issue #1367)
8
+
9
+ The `/merge` Telegram bot command now syncs the `ready` label between PRs and their linked issues before building the merge queue.
10
+ - 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
11
+ - 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
12
+ - Sync happens during `MergeQueueProcessor.initialize()`, before the final list of ready PRs is collected
13
+
3
14
  ## 1.25.8
4
15
 
5
16
  ### 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.0",
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,
@@ -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