@link-assistant/hive-mind 2.0.17 → 2.0.19

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
+ ## 2.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - 8061979: Preserve per-command isolation overrides for queued Telegram solve commands.
8
+
9
+ ## 2.0.18
10
+
11
+ ### Patch Changes
12
+
13
+ - 08c2f07: Clarify fork auto-recovery safety failures so terminal exits and failure comments explain the mismatched repository, expected upstream, safety-check result, and recovery options.
14
+
3
15
  ## 2.0.17
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": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -1,4 +1,5 @@
1
1
  import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
2
+ import { extractForkReplacementBlockedDetails, isForkReplacementBlockedReason } from './solve.repository-recovery-message.lib.mjs';
2
3
 
3
4
  export const FORK_DIVERGENCE_RESOLUTION_OPTION = '--allow-fork-divergence-resolution-using-force-push-with-lease';
4
5
 
@@ -29,6 +30,23 @@ export function buildPrePullRequestFailureActionSection(reason = '') {
29
30
  const normalizedReason = String(reason || '').toLowerCase();
30
31
  const isForkOrRecoveryFailure = normalizedReason.includes('fork') || normalizedReason.includes('auto-recovery') || normalizedReason.includes('repository setup');
31
32
 
33
+ if (isForkReplacementBlockedReason(reason)) {
34
+ const details = extractForkReplacementBlockedDetails(reason);
35
+ const repository = details.existingRepository || 'the existing repository';
36
+ const upstream = details.expectedUpstream || 'the expected upstream repository';
37
+
38
+ return `### What happened
39
+ - Hive Mind found \`${repository}\` where it needed a GitHub fork of \`${upstream}\`.
40
+ - It could not prove whether the existing repository has unique commits, so it did not delete it automatically.
41
+
42
+ ### What you can do
43
+ - If you control \`${repository}\`, back up any needed work, then delete, rename, archive, or repair it in GitHub and rerun the solver.
44
+ - If you do not control \`${repository}\`, ask the repository owner or a Hive Mind administrator to clean it up and rerun the solver.
45
+ - Use \`--allow-force-non-fork-repository-deletion\` only after confirming that deleting \`${repository}\` is acceptable and any unique commits can be lost.
46
+
47
+ Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
48
+ }
49
+
32
50
  if (isForkDivergenceFailure(reason)) {
33
51
  return `### What you can do
34
52
  - If the fork's default branch can be overwritten safely, rerun with \`${FORK_DIVERGENCE_RESOLUTION_OPTION}\` to allow a guarded force-with-lease sync.
@@ -0,0 +1,72 @@
1
+ export const FORK_REPLACEMENT_BLOCKED_REASON_PREFIX = 'Repository setup halted - existing fork replacement could lose commits.';
2
+
3
+ const fallback = (value, replacement) => {
4
+ const text = value === null || value === undefined ? '' : String(value).trim();
5
+ return text || replacement;
6
+ };
7
+
8
+ export function summarizeGitHubCompareFailure(output = '') {
9
+ const text = fallback(output, 'unknown compare error');
10
+ const status = text.match(/HTTP\s+(\d+)/i)?.[1] || text.match(/"status"\s*:\s*"?(\d+)"?/i)?.[1] || null;
11
+ const message = text.match(/"message"\s*:\s*"([^"]+)"/i)?.[1] || text.match(/gh:\s*([^\n(]+)/i)?.[1]?.trim() || null;
12
+
13
+ if (status && message) return `${status} ${message}`;
14
+ return message || status || text.split('\n')[0].slice(0, 160);
15
+ }
16
+
17
+ export function buildForkReplacementSafetyCheckDescription({ aheadBy = null, compareFailureOutput = '' } = {}) {
18
+ if (Number.isFinite(aheadBy) && aheadBy > 0) {
19
+ return `GitHub compare reported ${aheadBy} commit(s) ahead of upstream, so deleting the repository could lose commits.`;
20
+ }
21
+
22
+ if (compareFailureOutput) {
23
+ return `GitHub compare returned ${summarizeGitHubCompareFailure(compareFailureOutput)}, so Hive Mind could not prove the repository has no unique commits.`;
24
+ }
25
+
26
+ return 'Hive Mind could not prove the repository has no unique commits.';
27
+ }
28
+
29
+ export function buildForkReplacementBlockedReason({ existingRepository, expectedUpstream, relationshipDescription, safetyCheckDescription } = {}) {
30
+ const repository = fallback(existingRepository, 'the existing repository');
31
+ const upstream = fallback(expectedUpstream, 'the expected upstream repository');
32
+ const relationship = fallback(relationshipDescription, `${repository} is not a confirmed GitHub fork of ${upstream}.`);
33
+ const safetyCheck = fallback(safetyCheckDescription, buildForkReplacementSafetyCheckDescription());
34
+
35
+ return `${FORK_REPLACEMENT_BLOCKED_REASON_PREFIX}
36
+
37
+ What happened:
38
+ - Expected fork or replacement repository: ${repository}
39
+ - Expected upstream: ${upstream}
40
+ - Actual state: ${relationship}
41
+ - Safety check: ${safetyCheck}
42
+
43
+ Why it stopped:
44
+ Hive Mind did not delete ${repository} because the repository may contain commits that would be lost.
45
+
46
+ Options:
47
+ 1. Back up any needed work in ${repository}, then delete, rename, archive, or repair that repository in GitHub and rerun the solver.
48
+ 2. If you do not control ${repository}, ask the repository owner or a Hive Mind administrator to clean it up and rerun the solver.
49
+ 3. Rerun with --allow-force-non-fork-repository-deletion only after confirming that deleting ${repository} is acceptable.`;
50
+ }
51
+
52
+ export function isForkReplacementBlockedReason(reason = '') {
53
+ const text = String(reason || '');
54
+ const normalized = text.toLowerCase();
55
+ return normalized.includes(FORK_REPLACEMENT_BLOCKED_REASON_PREFIX.toLowerCase()) || normalized.includes('auto-recovery skipped - repository may contain commits that would be lost');
56
+ }
57
+
58
+ const extractLineValue = (reason, label) => {
59
+ const pattern = new RegExp(`^- ${label}:\\s*(.+)$`, 'im');
60
+ return (
61
+ String(reason || '')
62
+ .match(pattern)?.[1]
63
+ ?.trim() || null
64
+ );
65
+ };
66
+
67
+ export function extractForkReplacementBlockedDetails(reason = '') {
68
+ return {
69
+ existingRepository: extractLineValue(reason, 'Expected fork or replacement repository'),
70
+ expectedUpstream: extractLineValue(reason, 'Expected upstream'),
71
+ };
72
+ }
@@ -30,6 +30,7 @@ const { log, formatAligned } = lib;
30
30
  // Import exit handler
31
31
  import { safeExit } from './exit-handler.lib.mjs';
32
32
  import { parseForkFullNameFromGhOutput } from './github-repository-names.lib.mjs';
33
+ import { buildForkReplacementBlockedReason, buildForkReplacementSafetyCheckDescription } from './solve.repository-recovery-message.lib.mjs';
33
34
 
34
35
  // Import GitHub utilities for permission checks
35
36
  const githubLib = await import('./github.lib.mjs');
@@ -521,22 +522,30 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
521
522
  await log('');
522
523
  await log(`${formatAligned('⚠️', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'warning' });
523
524
  const detail = !forkValidation.isFork ? `Repository ${existingForkName} is NOT a GitHub fork (see issue #1518)` : `Fork ${existingForkName} was created from ${forkValidation.parent} instead of ${owner}/${repo} (see issue #967)`;
525
+ const relationshipDescription = !forkValidation.isFork ? `${existingForkName} is not a GitHub fork of ${owner}/${repo}.` : `${existingForkName} is a GitHub fork of ${forkValidation.parent || 'unknown parent'}, not ${owner}/${repo}.`;
524
526
  await log(`${formatAligned('', '', detail)}`);
525
527
  await log(`${formatAligned('', '', `Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}, source: ${forkValidation.source || 'N/A'}, expected: ${owner}/${repo}`)}`);
526
528
  // Safety check: compare commits before deleting to avoid data loss
527
529
  await log(`${formatAligned('🔍', 'Safety check:', 'Comparing commits against upstream...')}`);
528
530
  let safeToDelete = false;
531
+ let safetyCheckDescription = buildForkReplacementSafetyCheckDescription();
529
532
  try {
530
533
  const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --paginate --jq '.ahead_by' 2>&1`;
531
- if (cmp.code === 0 && parseInt(cmp.stdout.toString().trim(), 10) === 0) {
534
+ const aheadBy = parseInt(cmp.stdout.toString().trim(), 10);
535
+ if (cmp.code === 0 && aheadBy === 0) {
532
536
  await log(`${formatAligned('✅', 'Safe to delete:', 'No additional commits in non-fork repository')}`);
533
537
  safeToDelete = true;
534
538
  } else if (cmp.code === 0) {
535
- await log(`${formatAligned('⚠️', 'UNSAFE:', `Repository has ${cmp.stdout.toString().trim()} commit(s) ahead of upstream that would be lost`)}`, { level: 'warning' });
539
+ safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ aheadBy });
540
+ const aheadDescription = Number.isFinite(aheadBy) ? `${aheadBy} commit(s)` : 'an unknown number of commit(s)';
541
+ await log(`${formatAligned('⚠️', 'UNSAFE:', `Repository has ${aheadDescription} ahead of upstream that would be lost`)}`, { level: 'warning' });
536
542
  } else {
537
- await log(`${formatAligned('⚠️', 'Compare failed:', ((cmp.stderr?.toString() || '') + (cmp.stdout?.toString() || '')).split('\n')[0])}`, { level: 'warning' });
543
+ const compareOutput = (cmp.stderr?.toString() || '') + (cmp.stdout?.toString() || '');
544
+ safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ compareFailureOutput: compareOutput });
545
+ await log(`${formatAligned('⚠️', 'Compare failed:', compareOutput.split('\n')[0])}`, { level: 'warning' });
538
546
  }
539
547
  } catch (e) {
548
+ safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ compareFailureOutput: e.message });
540
549
  await log(`${formatAligned('⚠️', 'Compare error:', e.message)}`, { level: 'warning' });
541
550
  }
542
551
  if (!safeToDelete) {
@@ -547,7 +556,15 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
547
556
  await log(` 💡 Manual fix required: back up work, then: gh repo delete ${existingForkName} --yes`);
548
557
  await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
549
558
  await log(` 🔧 Or force deletion (DANGEROUS): solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-force-non-fork-repository-deletion`);
550
- await safeExit(1, 'Auto-recovery skipped - repository may contain commits that would be lost');
559
+ await safeExit(
560
+ 1,
561
+ buildForkReplacementBlockedReason({
562
+ existingRepository: existingForkName,
563
+ expectedUpstream: `${owner}/${repo}`,
564
+ relationshipDescription,
565
+ safetyCheckDescription,
566
+ })
567
+ );
551
568
  }
552
569
  }
553
570
  await log(`${formatAligned('🔄', 'Auto-recovery:', 'Deleting non-fork repository and creating fresh fork...')}`);
@@ -58,6 +58,8 @@ class SolveQueueItem {
58
58
  this.requester = options.requester;
59
59
  this.infoBlock = options.infoBlock;
60
60
  this.tool = options.tool || 'claude';
61
+ // Issue #1983: preserve per-command isolation through queued execution.
62
+ this.perCommandIsolation = options.perCommandIsolation || null;
61
63
  // Issue #1688: keep parsed URL context (owner/repo/number/type) so completion
62
64
  // notifications can look up linked PRs for issue URLs.
63
65
  this.urlContext = options.urlContext || null;