@link-assistant/hive-mind 1.24.3 → 1.24.5
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 +22 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +52 -10
- package/src/github-merge.lib.mjs +67 -35
- package/src/telegram-merge-queue.lib.mjs +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.24.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 17317bb: fix: prevent false positive error detection for JSON-structured stderr warnings (Issue #1337)
|
|
8
|
+
|
|
9
|
+
Claude Code SDK can emit structured JSON log messages to stderr with format `{"level":"warn","message":"..."}`. When these messages contained error-related keywords like "failed", the detection logic incorrectly flagged them as errors.
|
|
10
|
+
|
|
11
|
+
Added JSON parsing for stderr messages starting with `{`. If the parsed JSON has a `level` field that is not `"error"` or `"fatal"`, the message is treated as a warning (non-error), preserving existing emoji-prefix detection as a fallback.
|
|
12
|
+
|
|
13
|
+
Also enables `ANTHROPIC_LOG=debug` when running with `--verbose` flag, allowing users to see detailed API request information as suggested by the BashTool pre-flight warning.
|
|
14
|
+
|
|
15
|
+
## 1.24.4
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 40282f3: fix: escape '...' ellipsis in MarkdownV2 and retry on UNKNOWN merge state (Issue #1339)
|
|
20
|
+
|
|
21
|
+
Two root causes fixed:
|
|
22
|
+
1. **MarkdownV2 escaping**: In `formatProgressMessage()`, literal '...' was appended in PR titles, error messages, and overflow lines. Telegram's MarkdownV2 requires '.' to be escaped as '\.' - unescaped periods caused 400 Bad Request errors on every message update during CI wait.
|
|
23
|
+
2. **UNKNOWN merge state**: GitHub computes PR mergeability asynchronously, so initial queries may return `mergeStateStatus: 'UNKNOWN'`. The old code immediately skipped PRs in this state. Fixed by adding retry logic to `checkPRMergeable()` that retries up to 3 times with 5-second delays before giving up.
|
|
24
|
+
|
|
3
25
|
## 1.24.3
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -724,6 +724,48 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
724
724
|
throw new Error(`Failed to read session file: ${readError.message}`);
|
|
725
725
|
}
|
|
726
726
|
};
|
|
727
|
+
/**
|
|
728
|
+
* Determines whether a stderr message line should be treated as an error.
|
|
729
|
+
*
|
|
730
|
+
* Excludes:
|
|
731
|
+
* - Emoji-prefixed warnings (Issue #477): lines starting with ⚠️ or ⚠
|
|
732
|
+
* - JSON-structured log messages with non-error level (Issue #1337):
|
|
733
|
+
* e.g. {"level":"warn","message":"...failed..."} — the word "failed" is in
|
|
734
|
+
* the message text but the level is "warn", so it is NOT an error.
|
|
735
|
+
* Only JSON lines with level "error" or "fatal" are treated as real errors.
|
|
736
|
+
*
|
|
737
|
+
* @param {string} message - A single trimmed stderr line
|
|
738
|
+
* @returns {boolean} true if the line should count as an error
|
|
739
|
+
*/
|
|
740
|
+
export const isStderrError = message => {
|
|
741
|
+
const trimmed = message.trim();
|
|
742
|
+
if (!trimmed) return false;
|
|
743
|
+
|
|
744
|
+
// Detection 1: Emoji-prefixed warnings (Issue #477)
|
|
745
|
+
let isWarning = trimmed.startsWith('⚠️') || trimmed.startsWith('⚠');
|
|
746
|
+
|
|
747
|
+
// Detection 2: JSON-structured log messages (Issue #1337)
|
|
748
|
+
if (!isWarning && trimmed.startsWith('{')) {
|
|
749
|
+
try {
|
|
750
|
+
const parsed = JSON.parse(trimmed);
|
|
751
|
+
if (parsed && typeof parsed.level === 'string') {
|
|
752
|
+
const level = parsed.level.toLowerCase();
|
|
753
|
+
// Only "error" and "fatal" levels are real errors.
|
|
754
|
+
if (level !== 'error' && level !== 'fatal') {
|
|
755
|
+
isWarning = true;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} catch {
|
|
759
|
+
// Not valid JSON — fall through to keyword matching
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (!isWarning && (trimmed.includes('Error:') || trimmed.includes('error') || trimmed.includes('failed') || trimmed.includes('not found'))) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
return false;
|
|
767
|
+
};
|
|
768
|
+
|
|
727
769
|
export const executeClaudeCommand = async params => {
|
|
728
770
|
const {
|
|
729
771
|
tempDir,
|
|
@@ -847,10 +889,17 @@ export const executeClaudeCommand = async params => {
|
|
|
847
889
|
// Pass model for model-specific max output tokens (Issue #1221)
|
|
848
890
|
// Pass thinkLevel and maxBudget for Opus 4.6 effort level conversion (Issue #1238)
|
|
849
891
|
const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: mappedModel, thinkLevel, maxBudget });
|
|
892
|
+
// Issue #1337: Enable ANTHROPIC_LOG=debug in --verbose mode to diagnose slow API requests.
|
|
893
|
+
// The BashTool pre-flight check suggests "Run with ANTHROPIC_LOG=debug to check for failed or slow API requests."
|
|
894
|
+
// When --verbose is enabled, we propagate ANTHROPIC_LOG=debug so users can see detailed API request info.
|
|
895
|
+
if (argv.verbose) {
|
|
896
|
+
claudeEnv.ANTHROPIC_LOG = 'debug';
|
|
897
|
+
}
|
|
850
898
|
const modelMaxOutputTokens = getMaxOutputTokensForModel(mappedModel);
|
|
851
899
|
if (argv.verbose) await log(`📊 CLAUDE_CODE_MAX_OUTPUT_TOKENS: ${modelMaxOutputTokens}`, { verbose: true });
|
|
852
900
|
if (argv.verbose) await log(`📊 MCP_TIMEOUT: ${claudeCode.mcpTimeout}ms (server startup)`, { verbose: true });
|
|
853
901
|
if (argv.verbose) await log(`📊 MCP_TOOL_TIMEOUT: ${claudeCode.mcpToolTimeout}ms (tool execution)`, { verbose: true });
|
|
902
|
+
if (argv.verbose) await log(`📊 ANTHROPIC_LOG: debug (verbose mode)`, { verbose: true });
|
|
854
903
|
if (resolvedThinkingBudget !== undefined) await log(`📊 MAX_THINKING_TOKENS: ${resolvedThinkingBudget}`, { verbose: true });
|
|
855
904
|
if (claudeEnv.CLAUDE_CODE_EFFORT_LEVEL) await log(`📊 CLAUDE_CODE_EFFORT_LEVEL: ${claudeEnv.CLAUDE_CODE_EFFORT_LEVEL}`, { verbose: true });
|
|
856
905
|
if (!isNewVersion && thinkLevel) await log(`📊 Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
|
|
@@ -1065,16 +1114,9 @@ export const executeClaudeCommand = async params => {
|
|
|
1065
1114
|
// Log stderr immediately
|
|
1066
1115
|
if (errorOutput) {
|
|
1067
1116
|
await log(errorOutput, { stream: 'stderr' });
|
|
1068
|
-
// Track stderr errors for failure detection
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// Example: "⚠️ [BashTool] Pre-flight check is taking longer than expected. Run with ANTHROPIC_LOG=debug to check for failed or slow API requests."
|
|
1072
|
-
// Even though this contains the word "failed", it's a warning, not an error
|
|
1073
|
-
const isWarning = trimmed.startsWith('⚠️') || trimmed.startsWith('⚠');
|
|
1074
|
-
// Issue #1165: Also detect "command not found" errors (e.g., "/bin/sh: 1: claude: not found")
|
|
1075
|
-
// These indicate the Claude CLI is not installed or not in PATH
|
|
1076
|
-
if (trimmed && !isWarning && (trimmed.includes('Error:') || trimmed.includes('error') || trimmed.includes('failed') || trimmed.includes('not found'))) {
|
|
1077
|
-
stderrErrors.push(trimmed);
|
|
1117
|
+
// Track stderr errors for failure detection using shared helper (Issue #477, #1165, #1337)
|
|
1118
|
+
if (isStderrError(errorOutput)) {
|
|
1119
|
+
stderrErrors.push(errorOutput.trim());
|
|
1078
1120
|
}
|
|
1079
1121
|
}
|
|
1080
1122
|
} else if (chunk.type === 'exit') {
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -400,6 +400,11 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
|
|
|
400
400
|
|
|
401
401
|
/**
|
|
402
402
|
* Check if PR is mergeable
|
|
403
|
+
*
|
|
404
|
+
* Issue #1339: GitHub computes mergeability asynchronously. The first request may return
|
|
405
|
+
* mergeable: null and mergeStateStatus: 'UNKNOWN' while the computation is in progress.
|
|
406
|
+
* We retry up to 3 times with a 5-second delay between attempts to handle this case.
|
|
407
|
+
*
|
|
403
408
|
* @param {string} owner - Repository owner
|
|
404
409
|
* @param {string} repo - Repository name
|
|
405
410
|
* @param {number} prNumber - Pull request number
|
|
@@ -407,46 +412,73 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
|
|
|
407
412
|
* @returns {Promise<{mergeable: boolean, reason: string|null}>}
|
|
408
413
|
*/
|
|
409
414
|
export async function checkPRMergeable(owner, repo, prNumber, verbose = false) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
reason = `Merge state: ${pr.mergeStateStatus || 'unknown'}`;
|
|
415
|
+
// Issue #1339: GitHub computes mergeability asynchronously. When mergeStateStatus is
|
|
416
|
+
// 'UNKNOWN', it means GitHub hasn't calculated the merge state yet. Retry a few times.
|
|
417
|
+
const MAX_UNKNOWN_RETRIES = 3;
|
|
418
|
+
const UNKNOWN_RETRY_DELAY_MS = 5000;
|
|
419
|
+
|
|
420
|
+
for (let attempt = 0; attempt < MAX_UNKNOWN_RETRIES; attempt++) {
|
|
421
|
+
try {
|
|
422
|
+
const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json mergeable,mergeStateStatus`);
|
|
423
|
+
const pr = JSON.parse(stdout.trim());
|
|
424
|
+
|
|
425
|
+
// Issue #1339: If mergeStateStatus is 'UNKNOWN', GitHub is still computing.
|
|
426
|
+
// Wait and retry instead of immediately skipping the PR.
|
|
427
|
+
if (pr.mergeStateStatus === 'UNKNOWN' || pr.mergeable === null) {
|
|
428
|
+
if (attempt < MAX_UNKNOWN_RETRIES - 1) {
|
|
429
|
+
if (verbose) {
|
|
430
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} mergeability is UNKNOWN (attempt ${attempt + 1}/${MAX_UNKNOWN_RETRIES}), retrying in ${UNKNOWN_RETRY_DELAY_MS / 1000}s...`);
|
|
431
|
+
}
|
|
432
|
+
await new Promise(resolve => setTimeout(resolve, UNKNOWN_RETRY_DELAY_MS));
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
// All retries exhausted, still UNKNOWN - treat as not mergeable
|
|
436
|
+
if (verbose) {
|
|
437
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} mergeability still UNKNOWN after ${MAX_UNKNOWN_RETRIES} attempts`);
|
|
438
|
+
}
|
|
439
|
+
return { mergeable: false, reason: `Merge state: UNKNOWN (GitHub could not compute mergeability after ${MAX_UNKNOWN_RETRIES} attempts)` };
|
|
436
440
|
}
|
|
437
|
-
}
|
|
438
441
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
+
const mergeable = pr.mergeable === 'MERGEABLE';
|
|
443
|
+
let reason = null;
|
|
444
|
+
|
|
445
|
+
if (!mergeable) {
|
|
446
|
+
switch (pr.mergeStateStatus) {
|
|
447
|
+
case 'BLOCKED':
|
|
448
|
+
reason = 'PR is blocked (possibly by branch protection rules)';
|
|
449
|
+
break;
|
|
450
|
+
case 'BEHIND':
|
|
451
|
+
reason = 'PR branch is behind the base branch';
|
|
452
|
+
break;
|
|
453
|
+
case 'DIRTY':
|
|
454
|
+
reason = 'PR has merge conflicts';
|
|
455
|
+
break;
|
|
456
|
+
case 'UNSTABLE':
|
|
457
|
+
reason = 'PR has failing required status checks';
|
|
458
|
+
break;
|
|
459
|
+
case 'DRAFT':
|
|
460
|
+
reason = 'PR is a draft';
|
|
461
|
+
break;
|
|
462
|
+
default:
|
|
463
|
+
reason = `Merge state: ${pr.mergeStateStatus || 'unknown'}`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
442
466
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
467
|
+
if (verbose) {
|
|
468
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} mergeable: ${mergeable}, state: ${pr.mergeStateStatus}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { mergeable, reason };
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (verbose) {
|
|
474
|
+
console.log(`[VERBOSE] /merge: Error checking mergeability: ${error.message}`);
|
|
475
|
+
}
|
|
476
|
+
return { mergeable: false, reason: error.message };
|
|
447
477
|
}
|
|
448
|
-
return { mergeable: false, reason: error.message };
|
|
449
478
|
}
|
|
479
|
+
|
|
480
|
+
// Should not reach here, but return safe default
|
|
481
|
+
return { mergeable: false, reason: 'Merge state: UNKNOWN' };
|
|
450
482
|
}
|
|
451
483
|
|
|
452
484
|
/**
|
|
@@ -520,7 +520,7 @@ export class MergeQueueProcessor {
|
|
|
520
520
|
const elapsedSec = Math.round(this.targetBranchCIStatus.elapsedMs / 1000);
|
|
521
521
|
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
522
522
|
const elapsedSecRemainder = elapsedSec % 60;
|
|
523
|
-
message += `⏱️ Waiting for ${this.targetBranchCIStatus.count} CI run\\(s\\) on target branch to complete \\(${elapsedMin}m ${elapsedSecRemainder}s\\)
|
|
523
|
+
message += `⏱️ Waiting for ${this.targetBranchCIStatus.count} CI run\\(s\\) on target branch to complete \\(${elapsedMin}m ${elapsedSecRemainder}s\\)\\.\\.\\.\n\n`;
|
|
524
524
|
} else if (this.waitingForTargetBranchCI) {
|
|
525
525
|
message += `⏱️ Checking for active CI runs on target branch\\.\\.\\.\n\n`;
|
|
526
526
|
}
|
|
@@ -528,7 +528,8 @@ export class MergeQueueProcessor {
|
|
|
528
528
|
// Current item being processed
|
|
529
529
|
if (update.current && !this.waitingForTargetBranchCI) {
|
|
530
530
|
const statusEmoji = update.currentStatus === MergeItemStatus.WAITING_CI ? '⏱️' : '🔄';
|
|
531
|
-
|
|
531
|
+
// Issue #1339: escape the current item description for MarkdownV2
|
|
532
|
+
message += `${statusEmoji} ${this.escapeMarkdown(update.current)}\n\n`;
|
|
532
533
|
}
|
|
533
534
|
|
|
534
535
|
// Show errors/failures/skips inline so user gets immediate feedback (Issue #1269, #1294)
|
|
@@ -538,10 +539,12 @@ export class MergeQueueProcessor {
|
|
|
538
539
|
message += `⚠️ *Issues:*\n`;
|
|
539
540
|
for (const item of problemItems.slice(0, 5)) {
|
|
540
541
|
const statusEmoji = item.status === MergeItemStatus.FAILED ? '❌' : '⏭️';
|
|
541
|
-
|
|
542
|
+
// Issue #1339: escape the ellipsis '...' for MarkdownV2 (periods are reserved)
|
|
543
|
+
message += ` ${statusEmoji} \\#${item.prNumber}: ${this.escapeMarkdown(item.error.substring(0, 50))}${item.error.length > 50 ? '\\.\\.\\.' : ''}\n`;
|
|
542
544
|
}
|
|
543
545
|
if (problemItems.length > 5) {
|
|
544
|
-
|
|
546
|
+
// Issue #1339: escape the ellipsis '...' for MarkdownV2
|
|
547
|
+
message += ` _\\.\\.\\.and ${problemItems.length - 5} more issues_\n`;
|
|
545
548
|
}
|
|
546
549
|
message += '\n';
|
|
547
550
|
}
|
|
@@ -549,11 +552,13 @@ export class MergeQueueProcessor {
|
|
|
549
552
|
// PRs list with emojis
|
|
550
553
|
message += `*Queue:*\n`;
|
|
551
554
|
for (const item of update.items.slice(0, 10)) {
|
|
552
|
-
|
|
555
|
+
// Issue #1339: escape the ellipsis '...' for MarkdownV2 (periods are reserved)
|
|
556
|
+
message += `${item.emoji} \\#${item.prNumber}: ${this.escapeMarkdown(item.title.substring(0, 35))}${item.title.length > 35 ? '\\.\\.\\.' : ''}\n`;
|
|
553
557
|
}
|
|
554
558
|
|
|
555
559
|
if (update.items.length > 10) {
|
|
556
|
-
|
|
560
|
+
// Issue #1339: escape the ellipsis '...' for MarkdownV2
|
|
561
|
+
message += `_\\.\\.\\.and ${update.items.length - 10} more_\n`;
|
|
557
562
|
}
|
|
558
563
|
|
|
559
564
|
return message;
|