@link-assistant/hive-mind 1.50.12 → 1.50.14

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.50.14
4
+
5
+ ### Patch Changes
6
+
7
+ - f013f53: Fix PR mergeability consensus to ignore unrelated repo actions by default
8
+
9
+ ## 1.50.13
10
+
11
+ ### Patch Changes
12
+
13
+ - 3eb1428: Preserve GitHub issue-closing links after temporary auto-restart sessions edit pull request descriptions.
14
+
3
15
  ## 1.50.12
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -804,6 +804,126 @@ s=$(screen -ls | awk '/Detached/ {print $1; exit}'); echo "Entering $s"; screen
804
804
  s=$(screen -ls | awk '/Detached/ {last=$1} END{print last}'); echo "Entering $s"; screen -r "$s"; echo "Left $s";
805
805
  ```
806
806
 
807
+ ### Script for managing screens
808
+
809
+ ```bash
810
+ cat <<'EOF' > hive-screens.sh
811
+ #!/usr/bin/env bash
812
+
813
+ enter=false
814
+ close=false
815
+ oldest=false
816
+ newest=false
817
+ all=false
818
+
819
+ # --- parse args ---
820
+ for arg in "$@"; do
821
+ case "$arg" in
822
+ --enter) enter=true ;;
823
+ --close) close=true ;;
824
+ --oldest) oldest=true ;;
825
+ --newest) newest=true ;;
826
+ --all) all=true ;;
827
+ *)
828
+ echo "Unknown option: $arg"
829
+ exit 1
830
+ ;;
831
+ esac
832
+ done
833
+
834
+ # --- validate ---
835
+ if ! $enter && ! $close; then
836
+ echo "Must specify --enter or --close"
837
+ exit 1
838
+ fi
839
+
840
+ # --- default ---
841
+ if ! $oldest && ! $newest && ! $all; then
842
+ oldest=true
843
+ fi
844
+
845
+ matches=()
846
+
847
+ # --- sorting ---
848
+ if $newest; then
849
+ sorter="sort -nr"
850
+ else
851
+ sorter="sort -n"
852
+ fi
853
+
854
+ # --- scan sessions ---
855
+ while read -r sess; do
856
+ tmp=$(mktemp)
857
+ clean=$(mktemp)
858
+
859
+ # FIX: better capture
860
+ screen -S "$sess" -X scrollback 200000 2>/dev/null
861
+ sleep 0.15
862
+ screen -S "$sess" -X hardcopy -h "$tmp" 2>/dev/null
863
+
864
+ # strip garbage / non-printable chars
865
+ tr -cd '\11\12\15\40-\176' < "$tmp" > "$clean"
866
+
867
+ if grep -qi 'process completed' "$clean" &&
868
+ grep -qiE 'pr is mergeable!|pr merged!' "$clean"; then
869
+
870
+ log_path=$(tac "$clean" | grep -m1 -i 'full log file:' \
871
+ | sed 's/.*Full log file:[[:space:]]*//')
872
+
873
+ issue=$(tac "$clean" | grep -m1 -i 'Issue:[[:space:]]*https://github\.com/' \
874
+ | sed 's/Issue:[[:space:]]*//')
875
+
876
+ matches+=("$sess|$log_path|$issue")
877
+ fi
878
+
879
+ rm -f "$tmp" "$clean"
880
+ done < <(screen -ls | awk '/Detached/ {print $1}' | $sorter)
881
+
882
+ # --- no matches ---
883
+ if [ ${#matches[@]} -eq 0 ]; then
884
+ echo "No matching sessions"
885
+ exit 0
886
+ fi
887
+
888
+ process_one() {
889
+ IFS="|" read -r sess log issue <<< "$1"
890
+
891
+ echo "Session: $sess"
892
+
893
+ if $enter; then
894
+ echo "Entering $sess"
895
+ screen -r "$sess"
896
+ echo "Left $sess"
897
+ fi
898
+
899
+ [ -n "$log" ] && echo "Log: $log" || echo "Log: (not found)"
900
+ [ -n "$issue" ] && echo "Issue: $issue" || echo "Issue: (not found)"
901
+
902
+ if $close; then
903
+ echo "Closing $sess"
904
+ screen -S "$sess" -X stuff $'exit\n'
905
+ fi
906
+
907
+ echo "-----------------------------------"
908
+ }
909
+
910
+ # --- execution ---
911
+ if $all; then
912
+ for m in "${matches[@]}"; do
913
+ process_one "$m"
914
+ done
915
+ elif $oldest; then
916
+ process_one "${matches[0]}"
917
+ elif $newest; then
918
+ last_index=$((${#matches[@]} - 1))
919
+ process_one "${matches[$last_index]}"
920
+ fi
921
+
922
+ EOF
923
+
924
+ chmod +x hive-screens.sh
925
+ ```
926
+
807
927
  ### Reboot server.
808
928
 
809
929
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.50.12",
3
+ "version": "1.50.14",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "hive-telegram-bot": "./src/telegram-bot.mjs"
14
14
  },
15
15
  "scripts": {
16
- "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
16
+ "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
17
17
  "test:queue": "node tests/solve-queue.test.mjs",
18
18
  "test:limits-display": "node tests/limits-display.test.mjs",
19
19
  "test:usage-limit": "node tests/test-usage-limit.mjs",
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Helpers for preserving GitHub issue-closing links in pull request bodies.
5
+ */
6
+
7
+ import { hasGitHubLinkingKeyword } from './github-linking.lib.mjs';
8
+
9
+ /**
10
+ * Build the issue reference text to use in a GitHub closing keyword.
11
+ *
12
+ * @param {Object} options
13
+ * @param {string|number} options.issueNumber
14
+ * @param {string|null} [options.owner]
15
+ * @param {string|null} [options.repo]
16
+ * @param {boolean} [options.fork]
17
+ * @returns {string}
18
+ */
19
+ export function buildIssueReference({ issueNumber, owner = null, repo = null, fork = false } = {}) {
20
+ if (!issueNumber) {
21
+ return '';
22
+ }
23
+
24
+ if (fork && owner && repo) {
25
+ return `${owner}/${repo}#${issueNumber}`;
26
+ }
27
+
28
+ return `#${issueNumber}`;
29
+ }
30
+
31
+ /**
32
+ * Ensure a pull request body has a GitHub-recognized closing keyword for the issue.
33
+ *
34
+ * @param {string|null|undefined} prBody
35
+ * @param {Object} options
36
+ * @param {string|number} options.issueNumber
37
+ * @param {string|null} [options.owner]
38
+ * @param {string|null} [options.repo]
39
+ * @param {boolean} [options.fork]
40
+ * @returns {{body: string, updated: boolean, issueRef: string}}
41
+ */
42
+ export function ensureIssueLinkInPullRequestBody(prBody, { issueNumber, owner = null, repo = null, fork = false } = {}) {
43
+ const body = prBody ?? '';
44
+ const issueRef = buildIssueReference({ issueNumber, owner, repo, fork });
45
+
46
+ if (!issueNumber) {
47
+ return { body, updated: false, issueRef };
48
+ }
49
+
50
+ const hasLinkingKeyword = hasGitHubLinkingKeyword(body, issueNumber, owner, repo);
51
+ if (hasLinkingKeyword) {
52
+ return { body, updated: false, issueRef };
53
+ }
54
+
55
+ const separator = body.length > 0 ? '\n\n' : '';
56
+ return {
57
+ body: `${body}${separator}Fixes ${issueRef}`,
58
+ updated: true,
59
+ issueRef,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Parse GraphQL closingIssuesReferences stdout into issue number strings.
65
+ *
66
+ * @param {string|Buffer|null|undefined} output
67
+ * @returns {string[]}
68
+ */
69
+ export function parseClosingIssueNumbers(output) {
70
+ return String(output ?? '')
71
+ .trim()
72
+ .split('\n')
73
+ .map(value => value.trim())
74
+ .filter(Boolean);
75
+ }
76
+
77
+ /**
78
+ * Check whether parsed closing issue numbers contain the requested issue number.
79
+ *
80
+ * @param {Array<string|number>} linkedIssues
81
+ * @param {string|number} issueNumber
82
+ * @returns {boolean}
83
+ */
84
+ export function closingIssueNumbersContain(linkedIssues, issueNumber) {
85
+ if (!issueNumber || !Array.isArray(linkedIssues)) {
86
+ return false;
87
+ }
88
+
89
+ const expectedIssueNumber = String(issueNumber);
90
+ return linkedIssues.map(value => String(value).trim()).includes(expectedIssueNumber);
91
+ }
@@ -69,8 +69,8 @@ export const watchUntilMergeable = async params => {
69
69
  const MIN_CI_CHECK_INTERVAL_SECONDS = 120;
70
70
  const watchInterval = Math.max(rawWatchInterval, MIN_CI_CHECK_INTERVAL_SECONDS);
71
71
  const isAutoMerge = argv.autoMerge || false;
72
- // Issue #1503/#1573: --wait-for-all-actions-in-repository-before-mergeable
73
- // When enabled (default: true), blocks merge if ANY CI/CD run in the repo is active ensures safety when pipelines interact.
72
+ // Issue #1503/#1573/#1612: repo-wide action gating is opt-in strict mode.
73
+ // The config default may be bypassed when this module is reused directly, so normalize here.
74
74
  const waitForAllRepoActionsFlag = argv.waitForAllActionsInRepositoryBeforeMergeable ?? argv['wait-for-all-actions-in-repository-before-mergeable'] ?? argv.waitForAllActionsInRepositoryBeforeMergable ?? argv['wait-for-all-actions-in-repository-before-mergable'] ?? false;
75
75
 
76
76
  // Track latest session data across all iterations for accurate pricing
@@ -98,7 +98,7 @@ export const watchUntilMergeable = async params => {
98
98
  await log(formatAligned('', 'Mode:', isAutoMerge ? 'Auto-merge (will merge when ready)' : 'Auto-restart-until-mergeable (will NOT auto-merge)', 2));
99
99
  await log(formatAligned('', 'Checking interval:', `${watchInterval} seconds (minimum: ${MIN_CI_CHECK_INTERVAL_SECONDS}s)`, 2));
100
100
  await log(formatAligned('', 'Initial cooldown:', `${INITIAL_COOLDOWN_SECONDS} seconds`, 2));
101
- await log(formatAligned('', 'Wait for all repo actions:', waitForAllRepoActionsFlag ? 'Yes (absolute safety)' : 'No', 2));
101
+ await log(formatAligned('', 'Wait for all repo actions:', waitForAllRepoActionsFlag ? 'Yes (strict repo-wide safety)' : 'No (PR-scoped CI only)', 2));
102
102
  await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
103
103
  await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
104
104
  await log('');
@@ -3,6 +3,8 @@
3
3
  * Handles automatic creation of draft pull requests with initial commits
4
4
  */
5
5
 
6
+ import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
7
+
6
8
  export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
7
9
  // Skip auto-PR creation if:
8
10
  // 1. Auto-PR creation is disabled AND we're not in continue mode with no PR
@@ -1329,12 +1331,8 @@ ${prBody}`,
1329
1331
  const linkCheckResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { pullRequest(number: ${localPrNumber}) { closingIssuesReferences(first: 10) { nodes { number } } } } }' --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`;
1330
1332
 
1331
1333
  if (linkCheckResult.code === 0) {
1332
- const linkedIssues = linkCheckResult.stdout
1333
- .toString()
1334
- .trim()
1335
- .split('\n')
1336
- .filter(n => n);
1337
- if (linkedIssues.includes(issueNumber)) {
1334
+ const linkedIssues = parseClosingIssueNumbers(linkCheckResult.stdout);
1335
+ if (closingIssueNumbersContain(linkedIssues, issueNumber)) {
1338
1336
  await log(formatAligned('✅', 'Link verified:', `Issue #${issueNumber} → PR #${localPrNumber}`));
1339
1337
  } else {
1340
1338
  // This is a problem - the link wasn't created
@@ -188,8 +188,8 @@ export const SOLVE_OPTION_DEFINITIONS = {
188
188
  },
189
189
  'wait-for-all-actions-in-repository-before-mergeable': {
190
190
  type: 'boolean',
191
- description: 'Wait for ALL active GitHub Actions workflow runs in the entire repository to complete before declaring PR mergeable. When enabled, blocks merge if ANY CI/CD run in the repository is active, regardless of branch — this ensures safety when CI/CD pipelines interact or depend on each other. Enabled by default.',
192
- default: true,
191
+ description: 'Wait for ALL active GitHub Actions workflow runs in the entire repository to complete before declaring PR mergeable. When enabled, blocks merge if ANY CI/CD run in the repository is active, regardless of branch — this is a strict safety mode for repositories with cross-branch CI/CD coupling. Disabled by default.',
192
+ default: false,
193
193
  },
194
194
  'wait-for-all-actions-in-repository-before-mergable': {
195
195
  type: 'boolean',
package/src/solve.mjs CHANGED
@@ -29,7 +29,7 @@ const { processAutoContinueForIssue } = autoContinue;
29
29
  const repository = await import('./solve.repository.lib.mjs');
30
30
  const { setupTempDirectory, cleanupTempDirectory } = repository;
31
31
  const results = await import('./solve.results.lib.mjs');
32
- const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, buildSolveResumeCommand, checkForAiCreatedComments, attachSolutionSummary } = results;
32
+ const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, buildSolveResumeCommand, checkForAiCreatedComments, attachSolutionSummary, verifyPullRequestIssueLinkAfterAutoRestart } = results;
33
33
  const claudeLib = await import('./claude.lib.mjs');
34
34
  const { executeClaude, checkPlaywrightMcpAvailability } = claudeLib;
35
35
 
@@ -1358,6 +1358,8 @@ try {
1358
1358
  await log(` cd ${tempDir} && git push origin ${branchName}`, { level: 'error' });
1359
1359
  }
1360
1360
 
1361
+ await verifyPullRequestIssueLinkAfterAutoRestart({ prNumber, issueNumber, owner, repo, argv, cleanErrorMessage });
1362
+
1361
1363
  // Attach updated logs to PR after auto-restart completes
1362
1364
  // Issue #1154: Skip if logs were already uploaded by verifyResults() to prevent duplicates
1363
1365
  // Issue #1290: Always upload if auto-restart ran but last iteration's logs weren't uploaded
@@ -78,9 +78,9 @@ export const buildSolveResumeCommand = ({ issueUrl, sessionId, tool = null, mode
78
78
  const sentryLib = await import('./sentry.lib.mjs');
79
79
  const { reportError } = sentryLib;
80
80
 
81
- // Import GitHub linking detection library
82
- const githubLinking = await import('./github-linking.lib.mjs');
83
- const { hasGitHubLinkingKeyword } = githubLinking;
81
+ // Import pull request issue-link preservation helpers
82
+ const prIssueLinking = await import('./pr-issue-linking.lib.mjs');
83
+ const { buildIssueReference, ensureIssueLinkInPullRequestBody } = prIssueLinking;
84
84
 
85
85
  /**
86
86
  * Placeholder patterns used to detect auto-generated PR content that was not updated by the agent.
@@ -127,6 +127,83 @@ export const buildPRNotUpdatedHint = (titleNotUpdated, descriptionNotUpdated) =>
127
127
  return lines;
128
128
  };
129
129
 
130
+ /**
131
+ * Ensure an existing pull request body contains a GitHub closing keyword for the issue.
132
+ *
133
+ * @param {Object} options
134
+ * @param {string|number} options.prNumber - Pull request number
135
+ * @param {string|number} options.issueNumber - Issue number to link
136
+ * @param {string} options.owner - Repository owner
137
+ * @param {string} options.repo - Repository name
138
+ * @param {Object} [options.argv] - Parsed CLI arguments
139
+ * @param {Function} [options.command] - command-stream tagged template
140
+ * @param {Function} [options.logger] - Logger function
141
+ * @returns {Promise<{checked: boolean, updated: boolean, body: string, issueRef: string, error?: string}>}
142
+ */
143
+ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner, repo, argv = {}, command = $, logger = log }) => {
144
+ if (!prNumber || !issueNumber || !owner || !repo) {
145
+ return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error: 'missing required pull request or issue data' };
146
+ }
147
+
148
+ let prBody = '';
149
+ const prBodyResult = await command`gh pr view ${prNumber} --repo ${owner}/${repo} --json body --jq .body`;
150
+ if (prBodyResult.code !== 0) {
151
+ const error = prBodyResult.stderr ? prBodyResult.stderr.toString().trim() : 'Unknown error';
152
+ await logger(` ⚠️ Could not read PR body for issue link check: ${error}`);
153
+ return { checked: false, updated: false, body: prBody, issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error };
154
+ }
155
+
156
+ prBody = prBodyResult.stdout.toString();
157
+ const linkResult = ensureIssueLinkInPullRequestBody(prBody, {
158
+ issueNumber,
159
+ owner,
160
+ repo,
161
+ fork: argv.fork,
162
+ });
163
+
164
+ if (!linkResult.updated) {
165
+ await logger(' ✅ PR body already contains issue reference');
166
+ return { checked: true, updated: false, body: linkResult.body, issueRef: linkResult.issueRef };
167
+ }
168
+
169
+ await logger(` 📝 Updating PR body to link issue #${issueNumber}...`);
170
+
171
+ const fs = (await use('fs')).promises;
172
+ const tempBodyFile = `/tmp/pr-body-update-${prNumber}-${Date.now()}.md`;
173
+ await fs.writeFile(tempBodyFile, linkResult.body);
174
+
175
+ try {
176
+ const updateResult = await command`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
177
+ await fs.unlink(tempBodyFile).catch(() => {});
178
+
179
+ if (updateResult.code === 0) {
180
+ await logger(` ✅ Updated PR body to include "Fixes ${linkResult.issueRef}"`);
181
+ return { checked: true, updated: true, body: linkResult.body, issueRef: linkResult.issueRef };
182
+ }
183
+
184
+ const error = updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error';
185
+ await logger(` ⚠️ Could not update PR body: ${error}`);
186
+ return { checked: true, updated: false, body: prBody, issueRef: linkResult.issueRef, error };
187
+ } catch (updateError) {
188
+ await fs.unlink(tempBodyFile).catch(() => {});
189
+ throw updateError;
190
+ }
191
+ };
192
+
193
+ export const verifyPullRequestIssueLinkAfterAutoRestart = async ({ prNumber, issueNumber, owner, repo, argv = {}, cleanErrorMessage = error => error.message }) => {
194
+ if (!prNumber) {
195
+ return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }) };
196
+ }
197
+
198
+ await log('🔗 Verifying PR issue link after auto-restart...');
199
+ try {
200
+ return await ensurePullRequestIssueLink({ prNumber, issueNumber, owner, repo, argv });
201
+ } catch (issueLinkError) {
202
+ await log(`⚠️ Could not verify PR issue link after auto-restart: ${cleanErrorMessage(issueLinkError)}`, { level: 'warning' });
203
+ return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error: issueLinkError.message };
204
+ }
205
+ };
206
+
130
207
  /**
131
208
  * Detect the CLAUDE.md or .gitkeep commit hash from branch structure when not available in session
132
209
  * This handles continue mode where the commit hash was lost between sessions
@@ -622,52 +699,16 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
622
699
 
623
700
  // Skip PR body update and ready conversion for merged PRs (they can't be edited)
624
701
  if (!isPrMerged) {
625
- // Check if PR body has proper issue linking keywords
626
- let prBody = '';
627
- const prBodyResult = await $`gh pr view ${pr.number} --repo ${owner}/${repo} --json body --jq .body`;
628
- if (prBodyResult.code === 0) {
629
- prBody = prBodyResult.stdout.toString();
630
- const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
631
-
632
- // Use the new GitHub linking detection library to check for valid keywords
633
- // This ensures we only detect actual GitHub-recognized linking keywords
634
- // (fixes, closes, resolves and their variants) in proper format
635
- // See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
636
- const hasLinkingKeyword = hasGitHubLinkingKeyword(prBody, issueNumber, argv.fork ? owner : null, argv.fork ? repo : null);
637
-
638
- if (!hasLinkingKeyword) {
639
- await log(` 📝 Updating PR body to link issue #${issueNumber}...`);
640
-
641
- // Add proper issue reference to the PR body
642
- const linkingText = `\n\nFixes ${issueRef}`;
643
- const updatedBody = prBody + linkingText;
644
-
645
- // Use --body-file instead of --body to avoid command-line length limits
646
- // and special character escaping issues that can cause hangs/timeouts
647
- const fs = (await use('fs')).promises;
648
- const tempBodyFile = `/tmp/pr-body-update-${pr.number}-${Date.now()}.md`;
649
- await fs.writeFile(tempBodyFile, updatedBody);
650
-
651
- try {
652
- const updateResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
653
-
654
- // Clean up temp file
655
- await fs.unlink(tempBodyFile).catch(() => {});
656
-
657
- if (updateResult.code === 0) {
658
- await log(` ✅ Updated PR body to include "Fixes ${issueRef}"`);
659
- } else {
660
- await log(` ⚠️ Could not update PR body: ${updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error'}`);
661
- }
662
- } catch (updateError) {
663
- // Clean up temp file on error
664
- await fs.unlink(tempBodyFile).catch(() => {});
665
- throw updateError;
666
- }
667
- } else {
668
- await log(' ✅ PR body already contains issue reference');
669
- }
670
- }
702
+ const issueLinkResult = await ensurePullRequestIssueLink({
703
+ prNumber: pr.number,
704
+ issueNumber,
705
+ owner,
706
+ repo,
707
+ argv,
708
+ command: $,
709
+ logger: log,
710
+ });
711
+ const prBody = issueLinkResult.body || '';
671
712
 
672
713
  // Issue #1162: Detect if PR title/description still have auto-generated placeholder content
673
714
  // Track this before cleanup for --auto-restart-on-non-updated-pull-request-description
@@ -709,7 +750,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
709
750
 
710
751
  // Build new description
711
752
  const fs = (await use('fs')).promises;
712
- const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
753
+ const issueRef = buildIssueReference({ issueNumber, owner, repo, fork: argv.fork });
713
754
  const newDescription = `## Summary
714
755
 
715
756
  This pull request implements a solution for ${issueRef}: ${issueTitle}