@link-assistant/hive-mind 1.24.1 → 1.24.3
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 +17 -0
- package/README.md +14 -2
- package/package.json +1 -1
- package/src/claude.lib.mjs +51 -83
- package/src/config.lib.mjs +6 -2
- package/src/solve.auto-merge.lib.mjs +72 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.24.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 297e07c: Fix incorrect iteration counter and duplicate comments in auto-restart mode
|
|
8
|
+
- Fixed iteration counter to show actual AI restart count instead of check cycle number
|
|
9
|
+
- Added deduplication check to prevent duplicate "Ready to merge" status comments
|
|
10
|
+
- Added case study documentation for issue #1323
|
|
11
|
+
|
|
12
|
+
## 1.24.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- a74e10c: fix: add auto-resume with session preservation on Internal Server Error (Issue #1331)
|
|
17
|
+
|
|
18
|
+
When Claude tool returns `API Error: 500 Internal server error`, automatically retry with exponential backoff starting from 1 minute, capped at 30 minutes per retry, up to 10 retries. Session ID is preserved so Claude Code can resume from where it left off using `--resume <sessionId>`.
|
|
19
|
+
|
|
3
20
|
## 1.24.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -811,9 +811,21 @@ procinfo 62220
|
|
|
811
811
|
|
|
812
812
|
## Maintenance
|
|
813
813
|
|
|
814
|
-
###
|
|
814
|
+
### Enter latest screen
|
|
815
|
+
|
|
816
|
+
```bash
|
|
817
|
+
s=$(screen -ls | awk '/Detached/ {print $1; exit}'); echo "Entering $s"; screen -r "$s"; echo "Left $s";
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### Enter oldest screen
|
|
815
821
|
|
|
822
|
+
```bash
|
|
823
|
+
s=$(screen -ls | awk '/Detached/ {last=$1} END{print last}'); echo "Entering $s"; screen -r "$s"; echo "Left $s";
|
|
816
824
|
```
|
|
825
|
+
|
|
826
|
+
### Reboot server.
|
|
827
|
+
|
|
828
|
+
```bash
|
|
817
829
|
sudo reboot
|
|
818
830
|
```
|
|
819
831
|
|
|
@@ -821,7 +833,7 @@ That will remove all dangling unused proccesses and screens, which will in turn
|
|
|
821
833
|
|
|
822
834
|
### Cleanup disk space.
|
|
823
835
|
|
|
824
|
-
```
|
|
836
|
+
```bash
|
|
825
837
|
df -h
|
|
826
838
|
|
|
827
839
|
rm -rf /tmp
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -747,17 +747,30 @@ export const executeClaudeCommand = async params => {
|
|
|
747
747
|
repo,
|
|
748
748
|
prNumber,
|
|
749
749
|
} = params;
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
const baseDelay = timeouts.retryBaseDelay;
|
|
750
|
+
// Issue #1331: Unified retry configuration for all transient API errors
|
|
751
|
+
// (Overloaded, 503 Network Error, Internal Server Error) - same params, all with session preservation
|
|
753
752
|
let retryCount = 0;
|
|
753
|
+
// Helper: wait with per-minute countdown for delays >1 minute (Issue #1331)
|
|
754
|
+
const waitWithCountdown = async (delayMs, log) => {
|
|
755
|
+
if (delayMs <= 60000) {
|
|
756
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
let remaining = delayMs;
|
|
760
|
+
const timer = setInterval(async () => {
|
|
761
|
+
remaining -= 60000;
|
|
762
|
+
if (remaining > 0) await log(`⏳ ${Math.round(remaining / 60000)} min remaining...`);
|
|
763
|
+
}, 60000);
|
|
764
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
765
|
+
clearInterval(timer);
|
|
766
|
+
};
|
|
754
767
|
// Function to execute with retry logic
|
|
755
768
|
const executeWithRetry = async () => {
|
|
756
769
|
// Execute claude command from the cloned repository directory
|
|
757
770
|
if (retryCount === 0) {
|
|
758
771
|
await log(`\n${formatAligned('🤖', 'Executing Claude:', argv.model.toUpperCase())}`);
|
|
759
772
|
} else {
|
|
760
|
-
await log(`\n${formatAligned('🔄', 'Retry attempt:', `${retryCount}/${
|
|
773
|
+
await log(`\n${formatAligned('🔄', 'Retry attempt:', `${retryCount}/${retryLimits.maxTransientErrorRetries}`)}`);
|
|
761
774
|
}
|
|
762
775
|
if (argv.verbose) {
|
|
763
776
|
// Output the actual model being used
|
|
@@ -789,6 +802,7 @@ export const executeClaudeCommand = async params => {
|
|
|
789
802
|
let lastMessage = '';
|
|
790
803
|
let isOverloadError = false;
|
|
791
804
|
let is503Error = false;
|
|
805
|
+
let isInternalServerError = false; // Issue #1331: Track 500 Internal server error
|
|
792
806
|
let stderrErrors = [];
|
|
793
807
|
let anthropicTotalCostUSD = null; // Capture Anthropic's official total_cost_usd from result
|
|
794
808
|
let errorDuringExecution = false; // Issue #1088: Track if error_during_execution subtype occurred
|
|
@@ -979,6 +993,9 @@ export const executeClaudeCommand = async params => {
|
|
|
979
993
|
limitReached = true;
|
|
980
994
|
await log('⚠️ Detected session limit in result', { verbose: true });
|
|
981
995
|
}
|
|
996
|
+
if (lastMessage.includes('Internal server error') && !lastMessage.includes('Overloaded')) {
|
|
997
|
+
isInternalServerError = true;
|
|
998
|
+
}
|
|
982
999
|
}
|
|
983
1000
|
}
|
|
984
1001
|
// Store last message for error detection
|
|
@@ -986,6 +1003,9 @@ export const executeClaudeCommand = async params => {
|
|
|
986
1003
|
lastMessage = data.text;
|
|
987
1004
|
} else if (data.type === 'error') {
|
|
988
1005
|
lastMessage = data.error || JSON.stringify(data);
|
|
1006
|
+
if (lastMessage.includes('Internal server error')) {
|
|
1007
|
+
isInternalServerError = true;
|
|
1008
|
+
}
|
|
989
1009
|
}
|
|
990
1010
|
// Check for API overload error and 503 errors
|
|
991
1011
|
if (data.type === 'assistant' && data.message && data.message.content) {
|
|
@@ -998,6 +1018,10 @@ export const executeClaudeCommand = async params => {
|
|
|
998
1018
|
lastMessage = item.text;
|
|
999
1019
|
await log('⚠️ Detected API overload error', { verbose: true });
|
|
1000
1020
|
}
|
|
1021
|
+
if (item.text.includes('API Error: 500') && item.text.includes('Internal server error') && !item.text.includes('Overloaded')) {
|
|
1022
|
+
isInternalServerError = true;
|
|
1023
|
+
lastMessage = item.text;
|
|
1024
|
+
}
|
|
1001
1025
|
// Check for 503 errors
|
|
1002
1026
|
if (item.text.includes('API Error: 503') || (item.text.includes('503') && item.text.includes('upstream connect error')) || (item.text.includes('503') && item.text.includes('remote connection failure'))) {
|
|
1003
1027
|
is503Error = true;
|
|
@@ -1110,64 +1134,22 @@ export const executeClaudeCommand = async params => {
|
|
|
1110
1134
|
}
|
|
1111
1135
|
}
|
|
1112
1136
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
await
|
|
1121
|
-
// Increment retry count and retry
|
|
1122
|
-
retryCount++;
|
|
1123
|
-
return await executeWithRetry();
|
|
1124
|
-
} else {
|
|
1125
|
-
await log(`\n\n❌ API overload error persisted after ${maxRetries} retries\n The API appears to be heavily loaded. Please try again later.`, { level: 'error' });
|
|
1126
|
-
return {
|
|
1127
|
-
success: false,
|
|
1128
|
-
sessionId,
|
|
1129
|
-
limitReached: false,
|
|
1130
|
-
limitResetTime: null,
|
|
1131
|
-
limitTimezone: null,
|
|
1132
|
-
messageCount,
|
|
1133
|
-
toolUseCount,
|
|
1134
|
-
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1135
|
-
resultSummary, // Issue #1263: Include result summary
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
if ((commandFailed || is503Error) && argv.autoResumeOnErrors && (is503Error || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && lastMessage.includes('upstream connect error')) || (lastMessage.includes('503') && lastMessage.includes('remote connection failure')))) {
|
|
1140
|
-
if (retryCount < retryLimits.max503Retries) {
|
|
1141
|
-
// Calculate exponential backoff delay starting from 5 minutes
|
|
1142
|
-
const delay = retryLimits.initial503RetryDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount);
|
|
1143
|
-
const delayMinutes = Math.round(delay / (1000 * 60));
|
|
1144
|
-
await log(`\n⚠️ 503 network error detected. Retrying in ${delayMinutes} minutes...`, { level: 'warning' });
|
|
1137
|
+
// Issue #1331: Unified handler for all transient API errors (Overloaded, 503, Internal Server Error)
|
|
1138
|
+
// All use same params: 10 retries, 1min initial, 30min max, exponential backoff, session preserved
|
|
1139
|
+
const isTransientError = isOverloadError || isInternalServerError || is503Error || (lastMessage.includes('API Error: 500') && (lastMessage.includes('Overloaded') || lastMessage.includes('Internal server error'))) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')) || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && (lastMessage.includes('upstream connect error') || lastMessage.includes('remote connection failure')));
|
|
1140
|
+
if ((commandFailed || isTransientError) && isTransientError) {
|
|
1141
|
+
if (retryCount < retryLimits.maxTransientErrorRetries) {
|
|
1142
|
+
const delay = Math.min(retryLimits.initialTransientErrorDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount), retryLimits.maxTransientErrorDelayMs);
|
|
1143
|
+
const errorLabel = isOverloadError || (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded')) ? 'API overload (500)' : isInternalServerError || lastMessage.includes('Internal server error') ? 'Internal server error (500)' : '503 network error';
|
|
1144
|
+
await log(`\n⚠️ ${errorLabel} detected. Retry ${retryCount + 1}/${retryLimits.maxTransientErrorRetries} in ${Math.round(delay / 60000)} min (session preserved)...`, { level: 'warning' });
|
|
1145
1145
|
await log(` Error: ${lastMessage.substring(0, 200)}`, { verbose: true });
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
if (delay > 60000) {
|
|
1149
|
-
const countdownInterval = 60000; // Every minute
|
|
1150
|
-
let remainingMs = delay;
|
|
1151
|
-
const countdownTimer = setInterval(async () => {
|
|
1152
|
-
remainingMs -= countdownInterval;
|
|
1153
|
-
if (remainingMs > 0) {
|
|
1154
|
-
const remainingMinutes = Math.round(remainingMs / (1000 * 60));
|
|
1155
|
-
await log(`⏳ ${remainingMinutes} minutes remaining until retry...`);
|
|
1156
|
-
}
|
|
1157
|
-
}, countdownInterval);
|
|
1158
|
-
// Wait before retrying
|
|
1159
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1160
|
-
clearInterval(countdownTimer);
|
|
1161
|
-
} else {
|
|
1162
|
-
// Wait before retrying
|
|
1163
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1164
|
-
}
|
|
1146
|
+
if (sessionId && !argv.resume) argv.resume = sessionId; // preserve session for resume
|
|
1147
|
+
await waitWithCountdown(delay, log);
|
|
1165
1148
|
await log('\n🔄 Retrying now...');
|
|
1166
|
-
// Increment retry count and retry
|
|
1167
1149
|
retryCount++;
|
|
1168
1150
|
return await executeWithRetry();
|
|
1169
1151
|
} else {
|
|
1170
|
-
await log(`\n\n❌
|
|
1152
|
+
await log(`\n\n❌ Transient API error persisted after ${retryLimits.maxTransientErrorRetries} retries\n Please try again later or check https://status.anthropic.com/`, { level: 'error' });
|
|
1171
1153
|
return {
|
|
1172
1154
|
success: false,
|
|
1173
1155
|
sessionId,
|
|
@@ -1176,7 +1158,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1176
1158
|
limitTimezone: null,
|
|
1177
1159
|
messageCount,
|
|
1178
1160
|
toolUseCount,
|
|
1179
|
-
is503Error
|
|
1161
|
+
is503Error, // preserve for callers that check this
|
|
1180
1162
|
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1181
1163
|
resultSummary, // Issue #1263: Include result summary
|
|
1182
1164
|
};
|
|
@@ -1338,31 +1320,17 @@ export const executeClaudeCommand = async params => {
|
|
|
1338
1320
|
operation: 'run_claude_command',
|
|
1339
1321
|
});
|
|
1340
1322
|
const errorStr = error.message || error.toString();
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
return await executeWithRetry();
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
if (argv.autoResumeOnErrors && (errorStr.includes('API Error: 503') || (errorStr.includes('503') && errorStr.includes('upstream connect error')) || (errorStr.includes('503') && errorStr.includes('remote connection failure')))) {
|
|
1356
|
-
if (retryCount < retryLimits.max503Retries) {
|
|
1357
|
-
// Calculate exponential backoff delay starting from 5 minutes
|
|
1358
|
-
const delay = retryLimits.initial503RetryDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount);
|
|
1359
|
-
const delayMinutes = Math.round(delay / (1000 * 60));
|
|
1360
|
-
await log(`\n⚠️ 503 network error in exception. Retrying in ${delayMinutes} minutes...`, {
|
|
1361
|
-
level: 'warning',
|
|
1362
|
-
});
|
|
1363
|
-
// Wait before retrying
|
|
1364
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1365
|
-
// Increment retry count and retry
|
|
1323
|
+
// Issue #1331: Unified handler for all transient API errors in exception block
|
|
1324
|
+
// (Overloaded, 503, Internal Server Error) - same params, all with session preservation
|
|
1325
|
+
const isTransientException = (errorStr.includes('API Error: 500') && (errorStr.includes('Overloaded') || errorStr.includes('Internal server error'))) || (errorStr.includes('api_error') && errorStr.includes('Overloaded')) || errorStr.includes('API Error: 503') || (errorStr.includes('503') && (errorStr.includes('upstream connect error') || errorStr.includes('remote connection failure')));
|
|
1326
|
+
if (isTransientException) {
|
|
1327
|
+
if (retryCount < retryLimits.maxTransientErrorRetries) {
|
|
1328
|
+
const delay = Math.min(retryLimits.initialTransientErrorDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount), retryLimits.maxTransientErrorDelayMs);
|
|
1329
|
+
const errorLabel = errorStr.includes('Overloaded') ? 'API overload (500)' : errorStr.includes('Internal server error') ? 'Internal server error (500)' : '503 network error';
|
|
1330
|
+
await log(`\n⚠️ ${errorLabel} in exception. Retry ${retryCount + 1}/${retryLimits.maxTransientErrorRetries} in ${Math.round(delay / 60000)} min (session preserved)...`, { level: 'warning' });
|
|
1331
|
+
if (sessionId && !argv.resume) argv.resume = sessionId;
|
|
1332
|
+
await waitWithCountdown(delay, log);
|
|
1333
|
+
await log('\n🔄 Retrying now...');
|
|
1366
1334
|
retryCount++;
|
|
1367
1335
|
return await executeWithRetry();
|
|
1368
1336
|
}
|
package/src/config.lib.mjs
CHANGED
|
@@ -92,13 +92,17 @@ export const systemLimits = {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
// Retry configurations
|
|
95
|
+
// Issue #1331: All API error types use unified retry parameters:
|
|
96
|
+
// 10 max retries, 1 minute initial delay, 30 minute max delay (exponential backoff), session preserved
|
|
95
97
|
export const retryLimits = {
|
|
96
98
|
maxForkRetries: parseIntWithDefault('HIVE_MIND_MAX_FORK_RETRIES', 5),
|
|
97
99
|
maxVerifyRetries: parseIntWithDefault('HIVE_MIND_MAX_VERIFY_RETRIES', 5),
|
|
98
100
|
maxApiRetries: parseIntWithDefault('HIVE_MIND_MAX_API_RETRIES', 3),
|
|
99
101
|
retryBackoffMultiplier: parseFloatWithDefault('HIVE_MIND_RETRY_BACKOFF_MULTIPLIER', 2),
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// Unified retry config for all transient API errors (Overloaded, 503, Internal Server Error)
|
|
103
|
+
maxTransientErrorRetries: parseIntWithDefault('HIVE_MIND_MAX_TRANSIENT_ERROR_RETRIES', 10),
|
|
104
|
+
initialTransientErrorDelayMs: parseIntWithDefault('HIVE_MIND_INITIAL_TRANSIENT_ERROR_DELAY_MS', 60 * 1000), // 1 minute
|
|
105
|
+
maxTransientErrorDelayMs: parseIntWithDefault('HIVE_MIND_MAX_TRANSIENT_ERROR_DELAY_MS', 30 * 60 * 1000), // 30 minutes
|
|
102
106
|
};
|
|
103
107
|
|
|
104
108
|
// Claude Code CLI configurations
|
|
@@ -43,6 +43,37 @@ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
|
|
|
43
43
|
const restartShared = await import('./solve.restart-shared.lib.mjs');
|
|
44
44
|
const { checkPRMerged, checkPRClosed, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions, isApiError } = restartShared;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Issue #1323: Check if a comment with specific content already exists on the PR
|
|
48
|
+
* This prevents duplicate status comments when multiple processes or restarts occur
|
|
49
|
+
* @param {string} owner - Repository owner
|
|
50
|
+
* @param {string} repo - Repository name
|
|
51
|
+
* @param {number} prNumber - Pull request number
|
|
52
|
+
* @param {string} commentSignature - Unique signature to search for in comment body (e.g., "✅ Ready to merge")
|
|
53
|
+
* @param {boolean} verbose - Enable verbose logging
|
|
54
|
+
* @returns {Promise<boolean>} - True if a matching comment already exists
|
|
55
|
+
*/
|
|
56
|
+
const checkForExistingComment = async (owner, repo, prNumber, commentSignature, verbose = false) => {
|
|
57
|
+
try {
|
|
58
|
+
// Fetch recent PR comments (last 20 to avoid fetching entire history)
|
|
59
|
+
const result = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --jq '.[].body' 2>/dev/null`;
|
|
60
|
+
if (result.code === 0 && result.stdout) {
|
|
61
|
+
const bodies = result.stdout.toString();
|
|
62
|
+
const hasMatch = bodies.includes(commentSignature);
|
|
63
|
+
if (verbose && hasMatch) {
|
|
64
|
+
console.log(`[VERBOSE] Found existing comment with signature: "${commentSignature}"`);
|
|
65
|
+
}
|
|
66
|
+
return hasMatch;
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// If check fails, allow posting to avoid silent failures
|
|
70
|
+
if (verbose) {
|
|
71
|
+
console.log(`[VERBOSE] Failed to check for existing comment: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
76
|
+
|
|
46
77
|
/**
|
|
47
78
|
* Check for new comments from non-bot users since last commit
|
|
48
79
|
* @returns {Promise<{hasNewComments: boolean, comments: Array}>}
|
|
@@ -258,6 +289,11 @@ export const watchUntilMergeable = async params => {
|
|
|
258
289
|
let latestSessionId = null;
|
|
259
290
|
let latestAnthropicCost = null;
|
|
260
291
|
|
|
292
|
+
// Issue #1323: Track actual restart count separately from check cycle iteration
|
|
293
|
+
// `iteration` counts check cycles (how many times we check for blockers)
|
|
294
|
+
// `restartCount` counts actual AI tool executions (when we actually restart the AI)
|
|
295
|
+
let restartCount = 0;
|
|
296
|
+
|
|
261
297
|
// Track consecutive API errors for retry limit
|
|
262
298
|
const MAX_API_ERROR_RETRIES = 3;
|
|
263
299
|
let consecutiveApiErrors = 0;
|
|
@@ -344,10 +380,17 @@ export const watchUntilMergeable = async params => {
|
|
|
344
380
|
await log(formatAligned('', 'PR is ready to be merged manually', '', 2));
|
|
345
381
|
await log(formatAligned('', 'Exiting auto-restart-until-mergeable mode', '', 2));
|
|
346
382
|
|
|
347
|
-
// Post success comment
|
|
383
|
+
// Issue #1323: Post success comment only if one doesn't already exist
|
|
384
|
+
// This prevents duplicate comments when multiple processes reach this point
|
|
348
385
|
try {
|
|
349
|
-
const
|
|
350
|
-
|
|
386
|
+
const readyToMergeSignature = '## ✅ Ready to merge';
|
|
387
|
+
const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
|
|
388
|
+
if (!hasExistingComment) {
|
|
389
|
+
const commentBody = `## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n- All CI checks have passed\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
|
|
390
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
391
|
+
} else {
|
|
392
|
+
await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment', '', 2));
|
|
393
|
+
}
|
|
351
394
|
} catch {
|
|
352
395
|
// Don't fail if comment posting fails
|
|
353
396
|
}
|
|
@@ -518,10 +561,14 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
518
561
|
}
|
|
519
562
|
|
|
520
563
|
if (shouldRestart) {
|
|
564
|
+
// Issue #1323: Increment restart count (actual AI executions, not check cycles)
|
|
565
|
+
restartCount++;
|
|
566
|
+
|
|
521
567
|
// Add standard instructions for auto-restart-until-mergeable mode using shared utility
|
|
522
568
|
feedbackLines.push(...buildAutoRestartInstructions());
|
|
523
569
|
|
|
524
570
|
await log(formatAligned('🔄', 'RESTART TRIGGERED:', restartReason));
|
|
571
|
+
await log(formatAligned('', 'Restart iteration:', `${restartCount}`, 2));
|
|
525
572
|
await log('');
|
|
526
573
|
|
|
527
574
|
// Post a comment to PR about the restart
|
|
@@ -601,7 +648,8 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
601
648
|
try {
|
|
602
649
|
const logFile = getLogFile();
|
|
603
650
|
if (logFile) {
|
|
604
|
-
|
|
651
|
+
// Issue #1323: Use restartCount (actual AI executions) instead of iteration (check cycles)
|
|
652
|
+
const customTitle = `🔄 Auto-restart-until-mergeable Log (iteration ${restartCount})`;
|
|
605
653
|
await attachLogToGitHub({
|
|
606
654
|
logFile,
|
|
607
655
|
targetType: 'pr',
|
|
@@ -779,11 +827,17 @@ export const startAutoRestartUntilMergeable = async params => {
|
|
|
779
827
|
await log(formatAligned('', 'Action:', 'PR is ready for manual merge by a repository maintainer', 2));
|
|
780
828
|
await log('');
|
|
781
829
|
|
|
782
|
-
// Post a comment to the PR notifying the maintainer
|
|
830
|
+
// Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
|
|
783
831
|
try {
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
832
|
+
const readyToMergeSignature = '## ✅ Ready to merge';
|
|
833
|
+
const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
|
|
834
|
+
if (!hasExistingComment) {
|
|
835
|
+
const commentBody = `## ✅ Ready to merge\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because this PR was created from a fork (no write access to the target repository).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag (fork mode)*`;
|
|
836
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
837
|
+
await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
|
|
838
|
+
} else {
|
|
839
|
+
await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment', '', 2));
|
|
840
|
+
}
|
|
787
841
|
} catch {
|
|
788
842
|
// Don't fail if comment posting fails
|
|
789
843
|
}
|
|
@@ -802,11 +856,17 @@ export const startAutoRestartUntilMergeable = async params => {
|
|
|
802
856
|
await log(formatAligned('', 'Action:', 'PR is ready for manual merge by a repository maintainer', 2));
|
|
803
857
|
await log('');
|
|
804
858
|
|
|
805
|
-
// Post a comment to the PR notifying the maintainer
|
|
859
|
+
// Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
|
|
806
860
|
try {
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
861
|
+
const readyToMergeSignature = '## ✅ Ready to merge';
|
|
862
|
+
const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
|
|
863
|
+
if (!hasExistingComment) {
|
|
864
|
+
const commentBody = `## ✅ Ready to merge\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because the authenticated user lacks write access to \`${owner}/${repo}\` (current permission: \`${permission || 'unknown'}\`).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag*`;
|
|
865
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
866
|
+
await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
|
|
867
|
+
} else {
|
|
868
|
+
await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment', '', 2));
|
|
869
|
+
}
|
|
810
870
|
} catch {
|
|
811
871
|
// Don't fail if comment posting fails
|
|
812
872
|
}
|