@link-assistant/hive-mind 1.59.0 → 1.59.2

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,83 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.59.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 9e96635: Fix Telegram `/solve` repo-not-accessible message still suggesting `--auto-accept-invite` even when that flag is already active (issue #1714). After issue #1694 flipped `--auto-accept-invite` to default-on, `src/telegram-bot.mjs` was passing `autoAcceptInvite: args.some(a => a === '--auto-accept-invite')` to `validateGitHubEntityExistence()` — but the literal flag is no longer present in the typical default-on invocation, so the suppression added by issue #1692 silently regressed. The call now reads `parsedSolveArgs?.autoAcceptInvite` (matching the auto-accept pre-check two lines above), so the hint is suppressed when the flag is active and only shown when the user explicitly opts out with `--no-auto-accept-invite`. Adds `tests/test-issue-1714-auto-accept-invite-hint.mjs` covering the parsed-argv contract and a source-level guard against the `args.some(...)` form returning, plus a case study under `docs/case-studies/issue-1714/`.
8
+
9
+ ## 1.59.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 65d7b99: Fix misleading `/merge` verbose logs that read as "no CI configured" when CI was actually
14
+ running — addresses issue [#1712](https://github.com/link-assistant/hive-mind/issues/1712)
15
+ where a user mistakenly Ctrl+C'd the auto-restart-until-mergeable watcher after seeing:
16
+
17
+ ```
18
+ [VERBOSE] /merge: PR #83 has no CI checks yet - treating as no_checks
19
+ [VERBOSE] /merge: PR #83 has no CI check-runs yet, but 1 workflow run(s) were triggered ...
20
+ ⏳ Waiting for CI: Build and Release Docker Image
21
+ ```
22
+
23
+ The classification logic was correct — `/merge` was waiting on the legitimate 30-120s gap
24
+ between GitHub registering a `workflow_run` and publishing the corresponding `check_runs`.
25
+ The wording was the bug: "no CI checks yet" is parseable as "this repo has no CI", and the
26
+ listing showed run IDs without URLs, so the user couldn't quickly verify what `/merge` was
27
+ watching.
28
+
29
+ Changes:
30
+ - **`src/github-merge.lib.mjs`** — `getDetailedCIStatus` and `checkPRCIStatus` reword the
31
+ `no_checks` verbose lines to "has no check-runs or commit statuses registered yet",
32
+ including the short SHA. `getWorkflowRunsForSha` now appends `run.html_url` to every
33
+ entry. Normalized check-run / commit-status entries carry an `html_url` field
34
+ (falling back to `details_url` / `target_url`).
35
+ - **`src/solve.auto-merge-helpers.lib.mjs::getMergeBlockers`** — the `no_checks`,
36
+ `pending`, and `cancelled` branches now produce blocker `details` strings of the form
37
+ `"<name> [<status>] — <html_url>"`. The user-facing `⏳ Waiting for CI: …` line in
38
+ `solve.auto-merge.lib.mjs` (which joins `details` with commas) automatically picks up
39
+ the URLs, so the user can click through to the run.
40
+ - **`tests/test-misleading-merge-logs-1712.mjs`** — 13 unit tests covering the wording
41
+ guard, blocker enrichment for the no_checks / pending / cancelled paths, regression
42
+ guard for #1466, and the joined user-facing line format.
43
+ - **`docs/case-studies/issue-1712/README.md`** — full case study with raw logs, timeline,
44
+ root cause, fix description, and verification on the original PR
45
+ [link-foundation/box#83](https://github.com/link-foundation/box/pull/83) (which CI
46
+ passed for, after the user killed the watcher prematurely).
47
+
48
+ Also extends the `useWithRetry` helper (originally added in #1710 to recover from corrupt
49
+ hosted-CI npm-install state) with a third failure mode: `ERR_INVALID_PACKAGE_CONFIG` —
50
+ seen in this branch's own CI run when Node refused to parse a truncated
51
+ `getenv-v-latest/package.json`. `src/queue-config.lib.mjs` now loads `getenv` and
52
+ `links-notation` through the retry wrapper, matching `config.lib.mjs` and `lino.lib.mjs`.
53
+ Three new unit tests in `tests/test-use-with-retry.mjs` cover the new mode.
54
+
55
+ No upstream issue is needed — the bug was entirely in `link-assistant/hive-mind`. The
56
+ external workflow finished successfully (`check-runs-dfc4c14.json` shows `total_count: 22`).
57
+
58
+ **Follow-up round** (after review feedback in
59
+ [PR #1713 comment](https://github.com/link-assistant/hive-mind/pull/1713#issuecomment-4342387674)):
60
+ - **List active runs across ALL PR commits, not just HEAD.** New
61
+ `getActivePRWorkflowRuns()` in `src/github-merge-repo-actions.lib.mjs` walks every
62
+ commit on the PR (`/repos/.../pulls/N/commits`), dedupes by `run.id`, returns groups
63
+ marked `head` / `older`. The verbose log now lists active runs on older commits under
64
+ per-commit URL headers, so the GitHub Actions tab (which shows yellow dots for older
65
+ commits) reconciles with the log.
66
+ - **Eliminate duplicate logging.** `getWorkflowRunsForSha(verbose=true)` already prints
67
+ every run; the no_checks branch no longer re-iterates `workflowRuns`, just emits a
68
+ single explanatory summary line.
69
+ - **Commit URLs instead of short SHAs.** Verbose lines that referenced
70
+ `${sha.substring(0, 7)}` now use `https://github.com/${owner}/${repo}/commit/${sha}`
71
+ (or `/pull/N/commits/${sha}` where the PR context matters).
72
+ - **Inline plain-English explanations.** New `STATUS_HINTS` / `CONCLUSION_HINTS`
73
+ dictionaries plus `explainStatus()` helper — verbose lines read
74
+ `[in_progress] (currently executing)` instead of bare `in_progress`.
75
+ - **Multi-line user-facing waiting message.** The `⏳ Waiting for CI:` line is now
76
+ rendered by `renderBlocker()` — single-line for the common case (one run), but each
77
+ detail on its own indented line when there are multiple.
78
+ - 8 new tests added to `tests/test-misleading-merge-logs-1712.mjs` (Groups 5–8); 21
79
+ total. #1480 (31/31) and #1466 (14/14) regression suites still pass.
80
+
3
81
  ## 1.59.0
4
82
 
5
83
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.59.0",
3
+ "version": "1.59.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -91,6 +91,60 @@ export async function getPRCommitShas(owner, repo, prNumber, verbose = false) {
91
91
  }
92
92
  }
93
93
 
94
+ /**
95
+ * Issue #1712: Collect every active (in_progress / pending / queued / waiting / requested)
96
+ * workflow run on the PR branch — across ALL commits, not only the head SHA.
97
+ *
98
+ * Why this exists: when the user watches `/merge`, the GitHub Actions tab shows yellow
99
+ * dots for every commit that ever had a run, including older commits whose runs were
100
+ * automatically cancelled by GitHub's concurrency group. The verbose log used to list
101
+ * only the head-SHA runs, so a user comparing the log to the GitHub UI would see
102
+ * "1 workflow run" in the log but two yellow dots on screen — looking like a bug.
103
+ *
104
+ * Returns runs grouped by SHA, deduplicated by run.id (a single run can be associated
105
+ * with one SHA, but the same workflow file can produce runs on multiple SHAs).
106
+ *
107
+ * @param {string} owner - Repository owner
108
+ * @param {string} repo - Repository name
109
+ * @param {number} prNumber - Pull request number
110
+ * @param {string} headSha - The PR head SHA (used to mark which group is "current")
111
+ * @param {boolean} verbose - Whether to log verbose output
112
+ * @param {Function} getWorkflowRunsForSha - Function to get workflow runs for a SHA
113
+ * @returns {Promise<{groups: Array<{sha: string, isHead: boolean, runs: Array}>, totalActive: number, headActive: number, otherActive: number}>}
114
+ */
115
+ export async function getActivePRWorkflowRuns(owner, repo, prNumber, headSha, verbose, getWorkflowRunsForSha) {
116
+ const shas = await getPRCommitShas(owner, repo, prNumber, false);
117
+ if (shas.length === 0) {
118
+ return { groups: [], totalActive: 0, headActive: 0, otherActive: 0 };
119
+ }
120
+
121
+ const ACTIVE_STATUSES = new Set(['in_progress', 'pending', 'queued', 'waiting', 'requested']);
122
+ const groups = [];
123
+ const seenRunIds = new Set();
124
+ let totalActive = 0;
125
+ let headActive = 0;
126
+ let otherActive = 0;
127
+
128
+ for (const sha of shas) {
129
+ const runs = await getWorkflowRunsForSha(owner, repo, sha, false);
130
+ const activeRuns = runs.filter(r => ACTIVE_STATUSES.has(r.status) && !seenRunIds.has(r.id));
131
+ for (const r of activeRuns) seenRunIds.add(r.id);
132
+ if (activeRuns.length === 0) continue;
133
+
134
+ const isHead = sha === headSha;
135
+ groups.push({ sha, isHead, runs: activeRuns });
136
+ totalActive += activeRuns.length;
137
+ if (isHead) headActive += activeRuns.length;
138
+ else otherActive += activeRuns.length;
139
+ }
140
+
141
+ if (verbose && totalActive > 0) {
142
+ console.log(`[VERBOSE] pr-commits: ${totalActive} active workflow run(s) across ${groups.length} commit(s) on PR #${prNumber} (${headActive} on HEAD, ${otherActive} on older commits)`);
143
+ }
144
+
145
+ return { groups, totalActive, headActive, otherActive };
146
+ }
147
+
94
148
  /**
95
149
  * Check that workflow runs for ALL commits on the PR branch have completed.
96
150
  * @param {string} owner - Repository owner
@@ -342,7 +342,9 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
342
342
  // ([].every(fn) returns true for any fn).
343
343
  if (allChecks.length === 0) {
344
344
  if (verbose) {
345
- console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks yet - treating as pending`);
345
+ // Issue #1712: Reword for clarity empty check-runs API response, not "no CI configured".
346
+ const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
347
+ console.log(`[VERBOSE] /merge: PR #${prNumber} HEAD ${commitUrl} has no check-runs/statuses registered yet — treating as pending`);
346
348
  }
347
349
  return {
348
350
  status: 'pending',
@@ -1070,6 +1072,8 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
1070
1072
  const statuses = JSON.parse(statusJson.trim() || '[]');
1071
1073
 
1072
1074
  // Build detailed checks list
1075
+ // Issue #1712: Include html_url so the user-facing waiting message can include
1076
+ // a clickable link for each pending/failing check.
1073
1077
  const allChecks = [
1074
1078
  ...checkRuns.map(check => ({
1075
1079
  name: check.name,
@@ -1077,6 +1081,7 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
1077
1081
  conclusion: check.conclusion, // success, failure, cancelled, timed_out, skipped, neutral, action_required, stale, null
1078
1082
  type: 'check_run',
1079
1083
  id: check.id,
1084
+ html_url: check.html_url || check.details_url || null,
1080
1085
  })),
1081
1086
  ...statuses.map(status => ({
1082
1087
  name: status.context,
@@ -1084,13 +1089,20 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
1084
1089
  conclusion: status.state === 'pending' ? null : status.state === 'success' ? 'success' : status.state === 'failure' ? 'failure' : status.state,
1085
1090
  type: 'status',
1086
1091
  id: null,
1092
+ html_url: status.target_url || null,
1087
1093
  })),
1088
1094
  ];
1089
1095
 
1090
1096
  // No checks yet
1091
1097
  if (allChecks.length === 0) {
1092
1098
  if (verbose) {
1093
- console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks yet - treating as no_checks`);
1099
+ // Issue #1712: Reword to avoid "no CI checks" sounding like "no CI configured".
1100
+ // We are inspecting the GitHub `/commits/{sha}/check-runs` and `/commits/{sha}/status` endpoints;
1101
+ // an empty result means no check-runs/statuses are registered yet for this commit, NOT that the
1102
+ // repository has no CI/CD workflows. The caller (`getMergeBlockers`) decides between race
1103
+ // condition vs. "no CI configured" based on additional API calls.
1104
+ const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
1105
+ console.log(`[VERBOSE] /merge: PR #${prNumber} HEAD ${commitUrl} has no check-runs or commit statuses registered yet (status=no_checks; race vs. no-CI distinction is decided downstream)`);
1094
1106
  }
1095
1107
  return {
1096
1108
  status: 'no_checks',
@@ -1213,9 +1225,14 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
1213
1225
  .map(run => ({ id: run.id, status: run.status, conclusion: run.conclusion, name: run.name, html_url: run.html_url, path: run.path }));
1214
1226
 
1215
1227
  if (verbose) {
1216
- console.log(`[VERBOSE] /merge: Found ${runs.length} workflow runs for SHA ${sha.substring(0, 7)}`);
1228
+ // Issue #1712: Include the commit URL (not just SHA) and the workflow run html_url
1229
+ // so the user can click through to GitHub instead of pasting IDs into a URL by hand.
1230
+ // The run html_url already encodes the run ID, so we don't repeat it as a separate field.
1231
+ const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
1232
+ console.log(`[VERBOSE] /merge: Found ${runs.length} workflow run(s) for ${commitUrl}`);
1217
1233
  for (const run of runs) {
1218
- console.log(`[VERBOSE] /merge: - ${run.name} (${run.id}): status=${run.status}, conclusion=${run.conclusion}`);
1234
+ const concPart = run.conclusion ? `/${run.conclusion}` : '';
1235
+ console.log(`[VERBOSE] /merge: - ${run.name} [${run.status}${concPart}]: ${run.html_url}`);
1219
1236
  }
1220
1237
  }
1221
1238
 
@@ -1333,8 +1350,8 @@ export { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTrigger
1333
1350
  import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
1334
1351
  export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha };
1335
1352
 
1336
- import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas } from './github-merge-repo-actions.lib.mjs'; // Issue #1503
1337
- export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas };
1353
+ import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns } from './github-merge-repo-actions.lib.mjs'; // Issue #1503, #1712
1354
+ export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns };
1338
1355
 
1339
1356
  export default {
1340
1357
  READY_LABEL,
@@ -1376,4 +1393,5 @@ export default {
1376
1393
  getAllActiveRepoRuns,
1377
1394
  waitForAllRepoActions,
1378
1395
  checkCIConsensus, // Issue #1503
1396
+ getActivePRWorkflowRuns, // Issue #1712: list active runs across ALL PR commits
1379
1397
  };
@@ -36,10 +36,15 @@ if (typeof globalThis.use === 'undefined') {
36
36
  }
37
37
  }
38
38
 
39
- const getenvModule = await use('getenv');
39
+ // Issue #1710 / #1712: use-m occasionally hands back a truncated/corrupt global
40
+ // package on hosted CI (npm install -g flake — manifests as ERR_INVALID_PACKAGE_CONFIG,
41
+ // "Failed to resolve the path", or SyntaxError mid-import). useWithRetry deletes
42
+ // the broken install dir and re-fetches.
43
+ const { useWithRetry } = await import('./use-with-retry.lib.mjs');
44
+ const getenvModule = await useWithRetry(globalThis.use, 'getenv');
40
45
  // Node 24 CJS/ESM interop may return the whole module object instead of the function directly
41
46
  const getenv = typeof getenvModule === 'function' ? getenvModule : getenvModule.default || getenvModule;
42
- const linoModule = await use('links-notation');
47
+ const linoModule = await useWithRetry(globalThis.use, 'links-notation');
43
48
  const LinoParser = linoModule.Parser || linoModule.default?.Parser;
44
49
 
45
50
  /**
@@ -33,7 +33,43 @@ const { reportError } = sentryLib;
33
33
 
34
34
  // Import GitHub merge functions
35
35
  const githubMergeLib = await import('./github-merge.lib.mjs');
36
- const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getWorkflowRunJobsCount, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI } = githubMergeLib;
36
+ const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getWorkflowRunJobsCount, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI, getActivePRWorkflowRuns } = githubMergeLib;
37
+
38
+ /**
39
+ * Issue #1712: Plain-English meaning of GitHub Actions / check-run statuses, so the
40
+ * verbose log explains itself instead of forcing the user to look up GitHub docs.
41
+ * Returns the same status string suffixed with a parenthetical hint, e.g.
42
+ * "in_progress (currently executing)". Unknown statuses are returned unchanged.
43
+ */
44
+ const STATUS_HINTS = {
45
+ in_progress: 'currently executing',
46
+ queued: 'waiting for a runner',
47
+ pending: 'waiting to start',
48
+ waiting: 'blocked on a deployment / approval gate',
49
+ requested: 'requested but not yet picked up',
50
+ completed: 'finished',
51
+ };
52
+ const CONCLUSION_HINTS = {
53
+ success: 'passed',
54
+ failure: 'failed',
55
+ cancelled: 'cancelled (will be re-triggered if applicable)',
56
+ timed_out: 'timed out',
57
+ skipped: 'skipped (e.g. paths-ignore matched)',
58
+ neutral: 'neutral / informational',
59
+ action_required: 'manual approval required',
60
+ stale: 'stale — superseded by a newer run',
61
+ startup_failure: 'workflow failed to start (likely invalid YAML)',
62
+ };
63
+ const explainStatus = (status, conclusion) => {
64
+ const statusPart = status ? `${status}${STATUS_HINTS[status] ? ` (${STATUS_HINTS[status]})` : ''}` : 'unknown';
65
+ if (!conclusion) return statusPart;
66
+ const concPart = `${conclusion}${CONCLUSION_HINTS[conclusion] ? ` (${CONCLUSION_HINTS[conclusion]})` : ''}`;
67
+ return `${statusPart} → ${concPart}`;
68
+ };
69
+ const formatRunLine = run => {
70
+ const status = run.conclusion ? `${run.status}/${run.conclusion}` : run.status;
71
+ return `${run.name} [${status}] — ${run.html_url}`;
72
+ };
37
73
 
38
74
  // Issue #1625: Import centralized session-ending markers so the duplicate-
39
75
  // search scope for checkForExistingComment() stays in lock-step with the
@@ -343,13 +379,38 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
343
379
  }
344
380
 
345
381
  // Some workflow runs are still in progress or produced results — genuine race condition
382
+ // Issue #1712: User-facing blocker `details` carry the run URL + status so the
383
+ // top-level "⏳ Waiting for CI:" line is self-explanatory. Verbose listing is
384
+ // produced by `getWorkflowRunsForSha(..., verbose=true)` above — do NOT print the
385
+ // same run list twice. Here we only emit the one-line summary that explains
386
+ // *why* we're still waiting (race vs. real run).
387
+ const commitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${ciStatus.sha}`;
346
388
  if (verbose) {
347
- await log(`[VERBOSE] /merge: PR #${prNumber} has no CI check-runs yet, but ${workflowRuns.length} workflow run(s) were triggered for SHA ${ciStatus.sha.substring(0, 7)} - genuine race condition (waiting for check-runs to appear)`);
389
+ await log(`[VERBOSE] /merge: ${workflowRuns.length} workflow run(s) registered for PR #${prNumber} HEAD ${commitUrl} waiting for them to publish check-runs (race condition between workflow_run and check_runs APIs, typically ~30–120 s)`);
348
390
  }
391
+
392
+ // Also surface any active workflow runs on OLDER PR commits, so the user's view of
393
+ // the GitHub Actions tab (which shows yellow dots for every commit) reconciles
394
+ // with the log. These are NOT blockers — GitHub's concurrency group cancels them
395
+ // when a new commit is pushed — but listing them stops the user from worrying that
396
+ // the watcher is missing them.
397
+ const activeAcrossCommits = await getActivePRWorkflowRuns(owner, repo, prNumber, ciStatus.sha, verbose, getWorkflowRunsForSha);
398
+ if (verbose && activeAcrossCommits.otherActive > 0) {
399
+ await log(`[VERBOSE] /merge: ${activeAcrossCommits.otherActive} additional active workflow run(s) on older commits of PR #${prNumber} (these are not blockers — GitHub's concurrency group will cancel them):`);
400
+ for (const group of activeAcrossCommits.groups) {
401
+ if (group.isHead) continue;
402
+ const olderCommitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${group.sha}`;
403
+ await log(`[VERBOSE] /merge: on ${olderCommitUrl}:`);
404
+ for (const run of group.runs) {
405
+ await log(`[VERBOSE] /merge: - ${formatRunLine(run)} — ${explainStatus(run.status, run.conclusion)}`);
406
+ }
407
+ }
408
+ }
409
+
349
410
  blockers.push({
350
411
  type: 'ci_pending',
351
- message: `CI/CD checks have not started yet (${workflowRuns.length} workflow run(s) triggered, waiting for check-runs to appear)`,
352
- details: workflowRuns.map(r => r.name),
412
+ message: `Waiting for ${workflowRuns.length} workflow run(s) on HEAD ${commitUrl} to publish check-runs`,
413
+ details: workflowRuns.map(formatRunLine),
353
414
  });
354
415
  } else {
355
416
  // No workflow runs for this SHA — but this could be a race condition!
@@ -519,11 +580,27 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
519
580
  }
520
581
  } else if (ciStatus.status === 'pending') {
521
582
  // CI is still running or queued - wait for completion
522
- const pendingNames = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks].map(c => c.name);
583
+ const pendingChecks = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks];
584
+ const pendingDetails = pendingChecks.map(c => {
585
+ const statusPart = c.status ? ` [${c.status}]` : '';
586
+ const urlPart = c.html_url ? ` — ${c.html_url}` : '';
587
+ return `${c.name}${statusPart}${urlPart}`;
588
+ });
589
+ if (verbose) {
590
+ // Issue #1712: One concise line + a per-check entry that includes a plain-English
591
+ // explanation of the status. We do NOT also call getWorkflowRunsForSha here — the
592
+ // detailed CI status already covers the same data via check-runs.
593
+ const commitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${ciStatus.sha}`;
594
+ await log(`[VERBOSE] /merge: ${pendingChecks.length} check-run(s) still running/queued on PR #${prNumber} HEAD ${commitUrl}:`);
595
+ for (const c of pendingChecks) {
596
+ const url = c.html_url ? ` — ${c.html_url}` : '';
597
+ await log(`[VERBOSE] /merge: - ${c.name}: ${explainStatus(c.status, c.conclusion)}${url}`);
598
+ }
599
+ }
523
600
  blockers.push({
524
601
  type: 'ci_pending',
525
602
  message: 'CI/CD checks are still running or queued',
526
- details: pendingNames,
603
+ details: pendingDetails,
527
604
  });
528
605
  } else if (ciStatus.status === 'cancelled') {
529
606
  // All non-passed checks are cancelled or stale (no genuine failures)
@@ -540,10 +617,15 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
540
617
  } else {
541
618
  // These need to be re-triggered, NOT treated as AI-fixable failures
542
619
  const cancelledOrStaleChecks = [...ciStatus.cancelledChecks, ...(ciStatus.staleChecks || [])];
620
+ const cancelledDetails = cancelledOrStaleChecks.map(c => {
621
+ const concPart = c.conclusion ? ` [${c.conclusion}]` : '';
622
+ const urlPart = c.html_url ? ` — ${c.html_url}` : '';
623
+ return `${c.name}${concPart}${urlPart}`;
624
+ });
543
625
  blockers.push({
544
626
  type: 'ci_cancelled',
545
627
  message: 'CI/CD checks were cancelled or became stale',
546
- details: cancelledOrStaleChecks.map(c => c.name),
628
+ details: cancelledDetails,
547
629
  sha: ciStatus.sha,
548
630
  });
549
631
  }
@@ -903,10 +903,30 @@ No further AI sessions will be started automatically for this run. Please review
903
903
  const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
904
904
  const cancelledBlocker = blockers.find(b => b.type === 'ci_cancelled');
905
905
 
906
+ // Issue #1712: When `details` contain URLs (which they now always do for ci_pending /
907
+ // ci_cancelled blockers), comma-joining them produces an unreadable single-line wall
908
+ // of text. Render the first detail inline (with the message as the header) and any
909
+ // additional details on their own indented lines. Each detail is already
910
+ // self-explanatory: "<name> [<status>] — <url>".
911
+ const renderBlocker = (icon, header, blocker) => {
912
+ if (!blocker.details || blocker.details.length === 0) {
913
+ return log(formatAligned(icon, header, blocker.message, 2));
914
+ }
915
+ if (blocker.details.length === 1) {
916
+ return log(formatAligned(icon, header, blocker.details[0], 2));
917
+ }
918
+ return (async () => {
919
+ await log(formatAligned(icon, header, blocker.message, 2));
920
+ for (const detail of blocker.details) {
921
+ await log(formatAligned('', '', detail, 4));
922
+ }
923
+ })();
924
+ };
925
+
906
926
  if (cancelledOnly && cancelledBlocker) {
907
- await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
927
+ await renderBlocker('🔄', 'Waiting for re-triggered CI:', cancelledBlocker);
908
928
  } else if (pendingBlocker) {
909
- await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
929
+ await renderBlocker('⏳', 'Waiting for CI:', pendingBlocker);
910
930
  } else {
911
931
  await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
912
932
  }
@@ -967,7 +967,9 @@ async function handleSolveCommand(ctx) {
967
967
  VERBOSE && console.log(`[VERBOSE] Auto-accept invite pre-check failed: ${e.message}`);
968
968
  }
969
969
  }
970
- const entityCheck = await validateGitHubEntityExistence({ owner: validation.parsed.owner, repo: validation.parsed.repo, number: validation.parsed.number, type: validation.parsed.type, verbose: VERBOSE, autoAcceptInvite: args.some(a => a === '--auto-accept-invite') });
970
+ // Issue #1714: read the parsed argv (default-on per #1694) instead of the raw args list,
971
+ // so the invite hint is suppressed on the default-on path where the literal flag is absent.
972
+ const entityCheck = await validateGitHubEntityExistence({ owner: validation.parsed.owner, repo: validation.parsed.repo, number: validation.parsed.number, type: validation.parsed.type, verbose: VERBOSE, autoAcceptInvite: !!parsedSolveArgs?.autoAcceptInvite });
971
973
  if (!entityCheck.valid) {
972
974
  await safeReply(ctx, `❌ ${escapeMarkdown(entityCheck.error)}`, { reply_to_message_id: ctx.message.message_id });
973
975
  return;
@@ -4,7 +4,7 @@
4
4
  * Retry wrapper for `use-m` package loading.
5
5
  *
6
6
  * Issue #1710: Hosted CI runners occasionally hand back a truncated or
7
- * partially-installed global package after `npm install -g <pkg>`. Two
7
+ * partially-installed global package after `npm install -g <pkg>`. Three
8
8
  * surface symptoms have been observed:
9
9
  *
10
10
  * 1. `import` throws a SyntaxError ("Unexpected end of input") wrapped
@@ -13,8 +13,11 @@
13
13
  * 2. use-m throws `Failed to resolve the path to '<pkg>' from '<dir>'`
14
14
  * — the install completed without error but the package tree is
15
15
  * missing files that the `main`/`exports` entry depends on.
16
+ * 3. Node throws `Invalid package config <dir>/package.json.` with
17
+ * `code: 'ERR_INVALID_PACKAGE_CONFIG'` — the package.json itself
18
+ * is corrupt/truncated and cannot even be parsed (issue #1712).
16
19
  *
17
- * The recovery is identical for both: delete the broken alias install
20
+ * The recovery is identical for all three: delete the broken alias install
18
21
  * directory and ask use-m to re-fetch. A clean reinstall almost always
19
22
  * succeeds. This helper centralises that retry so every call site picks
20
23
  * it up.
@@ -68,10 +71,18 @@ export const isCorruptInstallError = error => {
68
71
  if (cause instanceof SyntaxError) return true;
69
72
  const causeMessage = typeof cause?.message === 'string' ? cause.message : '';
70
73
  if (/Unexpected end of input|Unexpected token/.test(causeMessage)) return true;
74
+ // Mode 3 (issue #1712): package.json itself is corrupt — Node refuses to
75
+ // even parse it and throws ERR_INVALID_PACKAGE_CONFIG before use-m's own
76
+ // resolve/import logic gets a chance to run.
77
+ if (error?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
78
+ if (cause?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
71
79
  // Mode 2 (also seen on hosted CI): npm install completes but the package
72
80
  // tree is incomplete, so use-m can't resolve the entry point.
73
81
  const message = typeof error?.message === 'string' ? error.message : '';
74
- return /^Failed to resolve the path to /.test(message);
82
+ if (/^Failed to resolve the path to /.test(message)) return true;
83
+ // Fallback string match for ERR_INVALID_PACKAGE_CONFIG (in case the error
84
+ // bubbles through use-m without preserving the `code` property).
85
+ return /^Invalid package config /.test(message);
75
86
  };
76
87
 
77
88
  export const extractCorruptedFilePath = error => {
@@ -82,7 +93,12 @@ export const extractCorruptedFilePath = error => {
82
93
  // is already the alias install directory — return it directly so callers
83
94
  // can clean it up (cleanup() handles both files and directories).
84
95
  const resolveMatch = message.match(/Failed to resolve the path to '[^']+' from '([^']+)'/);
85
- return resolveMatch ? resolveMatch[1] : null;
96
+ if (resolveMatch) return resolveMatch[1];
97
+ // Mode 3 (issue #1712): "Invalid package config <dir>/package.json." —
98
+ // extract the package.json path so the caller's cleanup() walks up to
99
+ // the alias dir.
100
+ const invalidConfigMatch = message.match(/Invalid package config (\S+?package\.json)/);
101
+ return invalidConfigMatch ? invalidConfigMatch[1] : null;
86
102
  };
87
103
 
88
104
  const defaultCleanup = async path => {