@link-assistant/hive-mind 1.25.4 → 1.25.6
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 +27 -0
- package/package.json +1 -1
- package/src/github-merge.lib.mjs +44 -0
- package/src/solve.auto-merge.lib.mjs +122 -36
- package/src/solve.restart-shared.lib.mjs +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.25.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5200c2a: Fix auto-restart spamming PR with comments when usage limit is reached (#1356)
|
|
8
|
+
|
|
9
|
+
When the AI tool's usage limit is reached during --auto-restart-until-mergeable mode, the loop now:
|
|
10
|
+
1. Detects the `limitReached` flag from the tool result
|
|
11
|
+
2. Silently waits for the limit reset time plus a 10-minute buffer (no GitHub comment posted)
|
|
12
|
+
3. Resumes the session using `--resume <sessionId>` with a "Continue" prompt, preserving context
|
|
13
|
+
|
|
14
|
+
For non-limit tool failures, the loop now stops immediately instead of retrying, preventing infinite loops on unrecoverable errors.
|
|
15
|
+
|
|
16
|
+
## 1.25.5
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- e0d68a4: fix: prevent false positive 'Ready to merge' for repos with CI but no required branch protection (Issue #1363)
|
|
21
|
+
|
|
22
|
+
Previously, the auto-merge logic would incorrectly declare a PR "Ready to merge — no CI/CD configured" when a repository had GitHub Actions workflows but no required status checks in branch protection rules. This happened because:
|
|
23
|
+
- `mergeStateStatus=CLEAN` (no required checks to block merging)
|
|
24
|
+
- `check_runs=[]` (CI hadn't started yet — race condition, GitHub takes ~10-30s to register checks)
|
|
25
|
+
|
|
26
|
+
The fix adds a workflow detection step (`getActiveRepoWorkflows`) that queries the GitHub Actions API to check if the repository has any active workflows. When workflows exist but no checks have started, the system now correctly identifies this as a race condition (CI hasn't started yet) rather than "no CI configured", and waits for the checks to appear before proceeding.
|
|
27
|
+
|
|
28
|
+
Full case study analysis in `docs/case-studies/issue-1363/`.
|
|
29
|
+
|
|
3
30
|
## 1.25.4
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/package.json
CHANGED
package/src/github-merge.lib.mjs
CHANGED
|
@@ -1227,6 +1227,48 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
|
1227
1227
|
}
|
|
1228
1228
|
}
|
|
1229
1229
|
|
|
1230
|
+
/**
|
|
1231
|
+
* Get the count of active (enabled) GitHub Actions workflows in a repository
|
|
1232
|
+
* Issue #1363: Used to distinguish between "no CI configured" and "CI hasn't started yet"
|
|
1233
|
+
*
|
|
1234
|
+
* When a repo has NO workflows, no_checks means no CI configured.
|
|
1235
|
+
* When a repo HAS workflows, no_checks means CI checks haven't started yet (race condition).
|
|
1236
|
+
*
|
|
1237
|
+
* @param {string} owner - Repository owner
|
|
1238
|
+
* @param {string} repo - Repository name
|
|
1239
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1240
|
+
* @returns {Promise<{count: number, hasWorkflows: boolean, workflows: Array<{id: number, name: string, state: string}>}>}
|
|
1241
|
+
*/
|
|
1242
|
+
export async function getActiveRepoWorkflows(owner, repo, verbose = false) {
|
|
1243
|
+
try {
|
|
1244
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/workflows" --jq '[.workflows[] | select(.state == "active")] | map({id: .id, name: .name, state: .state})'`);
|
|
1245
|
+
const workflows = JSON.parse(stdout.trim() || '[]');
|
|
1246
|
+
|
|
1247
|
+
if (verbose) {
|
|
1248
|
+
console.log(`[VERBOSE] /merge: Found ${workflows.length} active workflows in ${owner}/${repo}`);
|
|
1249
|
+
for (const wf of workflows) {
|
|
1250
|
+
console.log(`[VERBOSE] /merge: - ${wf.name} (${wf.id}): ${wf.state}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return {
|
|
1255
|
+
count: workflows.length,
|
|
1256
|
+
hasWorkflows: workflows.length > 0,
|
|
1257
|
+
workflows,
|
|
1258
|
+
};
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (verbose) {
|
|
1261
|
+
console.log(`[VERBOSE] /merge: Error fetching workflows for ${owner}/${repo}: ${error.message}`);
|
|
1262
|
+
}
|
|
1263
|
+
// On error, assume no workflows (safer: avoids false positives in the no-CI case)
|
|
1264
|
+
return {
|
|
1265
|
+
count: 0,
|
|
1266
|
+
hasWorkflows: false,
|
|
1267
|
+
workflows: [],
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1230
1272
|
// Issue #1341: Import and re-export post-merge CI functions from separate module
|
|
1231
1273
|
// to keep this file under the 1500 line limit
|
|
1232
1274
|
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
@@ -1265,4 +1307,6 @@ export default {
|
|
|
1265
1307
|
waitForCommitCI,
|
|
1266
1308
|
checkBranchCIHealth,
|
|
1267
1309
|
getMergeCommitSha,
|
|
1310
|
+
// Issue #1363: Detect active workflows to distinguish "no CI" from race condition
|
|
1311
|
+
getActiveRepoWorkflows,
|
|
1268
1312
|
};
|
|
@@ -33,7 +33,7 @@ const { reportError } = sentryLib;
|
|
|
33
33
|
|
|
34
34
|
// Import GitHub merge functions
|
|
35
35
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
|
-
const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha } = githubMergeLib;
|
|
36
|
+
const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows } = githubMergeLib;
|
|
37
37
|
|
|
38
38
|
// Import GitHub functions for log attachment
|
|
39
39
|
const githubLib = await import('./github.lib.mjs');
|
|
@@ -41,7 +41,14 @@ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
|
|
|
41
41
|
|
|
42
42
|
// Import shared utilities from the restart-shared module
|
|
43
43
|
const restartShared = await import('./solve.restart-shared.lib.mjs');
|
|
44
|
-
const { checkPRMerged, checkPRClosed, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions,
|
|
44
|
+
const { checkPRMerged, checkPRClosed, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions, isUsageLimitReached } = restartShared;
|
|
45
|
+
|
|
46
|
+
// Import validation functions for time parsing (used for usage limit wait)
|
|
47
|
+
const validation = await import('./solve.validation.lib.mjs');
|
|
48
|
+
const { calculateWaitTime } = validation;
|
|
49
|
+
|
|
50
|
+
// Import configuration (used for limit reset buffer and jitter)
|
|
51
|
+
import { limitReset } from './config.lib.mjs';
|
|
45
52
|
|
|
46
53
|
/**
|
|
47
54
|
* Issue #1323: Check if a comment with specific content already exists on the PR
|
|
@@ -197,14 +204,38 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
197
204
|
// Otherwise (e.g. mergeStateStatus === 'BLOCKED'), treat as pending race condition.
|
|
198
205
|
const earlyMergeStatus = await checkPRMergeable(owner, repo, prNumber, verbose);
|
|
199
206
|
if (earlyMergeStatus.mergeable) {
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
// Issue #1363: Before concluding "no CI configured", verify the repo actually
|
|
208
|
+
// has no active GitHub Actions workflows. If workflows exist but no checks have
|
|
209
|
+
// started yet, this is a race condition (GitHub takes ~10-30s to register checks
|
|
210
|
+
// after a push), NOT a "no CI configured" situation.
|
|
211
|
+
//
|
|
212
|
+
// This fixes a false positive where a repo with CI workflows but WITHOUT branch
|
|
213
|
+
// protection (required status checks) would be declared "no CI configured" because:
|
|
214
|
+
// - mergeStateStatus=CLEAN (no required checks to block it)
|
|
215
|
+
// - check_runs=[] (CI hasn't started yet — race condition)
|
|
216
|
+
const repoWorkflows = await getActiveRepoWorkflows(owner, repo, verbose);
|
|
217
|
+
if (repoWorkflows.hasWorkflows) {
|
|
218
|
+
// Repo HAS workflows — this is a race condition, not "no CI configured"
|
|
219
|
+
// Wait for CI checks to appear
|
|
220
|
+
if (verbose) {
|
|
221
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks yet, but repo has ${repoWorkflows.count} active workflow(s) - treating as race condition (CI hasn't started)`);
|
|
222
|
+
}
|
|
223
|
+
blockers.push({
|
|
224
|
+
type: 'ci_pending',
|
|
225
|
+
message: `CI/CD checks have not started yet (${repoWorkflows.count} workflow(s) configured, waiting for checks to appear)`,
|
|
226
|
+
details: repoWorkflows.workflows.map(wf => wf.name),
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
// Repo has NO workflows — this is truly "no CI configured"
|
|
230
|
+
// PR is already mergeable with no CI checks configured.
|
|
231
|
+
// Do NOT add a ci_pending blocker. The mergeability check below will also
|
|
232
|
+
// confirm this is mergeable, so blockers will be empty → PR IS MERGEABLE path.
|
|
233
|
+
if (verbose) {
|
|
234
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and repo has no active workflows - no CI/CD configured`);
|
|
235
|
+
}
|
|
236
|
+
// Return early with no CI blocker, mergeability already confirmed
|
|
237
|
+
return { blockers, ciStatus, noCiConfigured: true };
|
|
205
238
|
}
|
|
206
|
-
// Return early with no CI blocker, mergeability already confirmed
|
|
207
|
-
return { blockers, ciStatus, noCiConfigured: true };
|
|
208
239
|
} else {
|
|
209
240
|
// PR is not yet mergeable despite no checks - treat as pending race condition
|
|
210
241
|
blockers.push({
|
|
@@ -314,9 +345,6 @@ export const watchUntilMergeable = async params => {
|
|
|
314
345
|
// `restartCount` counts actual AI tool executions (when we actually restart the AI)
|
|
315
346
|
let restartCount = 0;
|
|
316
347
|
|
|
317
|
-
// Track consecutive API errors for retry limit
|
|
318
|
-
const MAX_API_ERROR_RETRIES = 3;
|
|
319
|
-
let consecutiveApiErrors = 0;
|
|
320
348
|
let currentBackoffSeconds = watchInterval;
|
|
321
349
|
|
|
322
350
|
await log('');
|
|
@@ -596,8 +624,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
596
624
|
await log('');
|
|
597
625
|
|
|
598
626
|
// Post a comment to PR about the restart
|
|
627
|
+
// Issue #1356: Include restart count for tracking and add deduplication
|
|
599
628
|
try {
|
|
600
|
-
const commentBody = `## 🔄 Auto-restart triggered\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
|
|
629
|
+
const commentBody = `## 🔄 Auto-restart triggered (attempt ${restartCount})\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
|
|
601
630
|
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
602
631
|
await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
|
|
603
632
|
} catch (commentError) {
|
|
@@ -632,33 +661,90 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
632
661
|
});
|
|
633
662
|
|
|
634
663
|
if (!toolResult.success) {
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
664
|
+
// Issue #1356: Check for usage limit errors FIRST (most specific)
|
|
665
|
+
// When usage limit is reached, silently wait for limitResetTime + buffer + jitter,
|
|
666
|
+
// then resume the session using --resume <sessionId> with a "Continue" prompt.
|
|
667
|
+
// No GitHub comment is posted — only log output.
|
|
668
|
+
if (isUsageLimitReached(toolResult)) {
|
|
669
|
+
const resumeSessionId = toolResult.sessionId;
|
|
670
|
+
const resetTime = toolResult.limitResetTime;
|
|
671
|
+
const baseWaitMs = resetTime ? calculateWaitTime(resetTime) : 0;
|
|
672
|
+
const bufferMs = limitReset.bufferMs;
|
|
673
|
+
const jitterMs = Math.floor(Math.random() * limitReset.jitterMs);
|
|
674
|
+
const waitMs = baseWaitMs + bufferMs + jitterMs;
|
|
675
|
+
const bufferMinutes = Math.round(bufferMs / 60000);
|
|
676
|
+
const jitterSeconds = Math.round(jitterMs / 1000);
|
|
677
|
+
const waitMinutes = Math.round(waitMs / 60000);
|
|
678
|
+
|
|
679
|
+
await log('');
|
|
680
|
+
await log(formatAligned('⏳', 'USAGE LIMIT REACHED', ''));
|
|
681
|
+
await log(formatAligned('', 'Reset time:', resetTime || 'Unknown', 2));
|
|
682
|
+
await log(formatAligned('', 'Waiting:', `${waitMinutes} min (reset + ${bufferMinutes} min buffer + ${jitterSeconds}s jitter)`, 2));
|
|
683
|
+
await log(formatAligned('', 'Action:', 'Silently waiting then resuming — no GitHub comment posted', 2));
|
|
684
|
+
if (resumeSessionId) {
|
|
685
|
+
await log(formatAligned('', 'Session ID:', resumeSessionId, 2));
|
|
646
686
|
}
|
|
687
|
+
await log('');
|
|
647
688
|
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
689
|
+
// Wait silently until the limit resets (no GitHub comment)
|
|
690
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
691
|
+
|
|
692
|
+
await log(formatAligned('✅', 'Usage limit wait complete', 'Resuming session...'));
|
|
693
|
+
await log('');
|
|
694
|
+
|
|
695
|
+
// Resume the session: execute with --resume <sessionId> and a "Continue" prompt
|
|
696
|
+
// This preserves context and the system message from the original session
|
|
697
|
+
if (resumeSessionId) {
|
|
698
|
+
const resumeArgv = { ...argv, resume: resumeSessionId };
|
|
699
|
+
const resumeResult = await executeToolIteration({
|
|
700
|
+
issueUrl,
|
|
701
|
+
owner,
|
|
702
|
+
repo,
|
|
703
|
+
issueNumber,
|
|
704
|
+
prNumber,
|
|
705
|
+
branchName: prBranch || branchName,
|
|
706
|
+
tempDir,
|
|
707
|
+
mergeStateStatus,
|
|
708
|
+
feedbackLines: ['Continue'],
|
|
709
|
+
argv: resumeArgv,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
if (resumeResult.success) {
|
|
713
|
+
// Resume succeeded - capture session data
|
|
714
|
+
currentBackoffSeconds = watchInterval;
|
|
715
|
+
if (resumeResult.sessionId) {
|
|
716
|
+
latestSessionId = resumeResult.sessionId;
|
|
717
|
+
latestAnthropicCost = resumeResult.anthropicTotalCostUSD;
|
|
718
|
+
}
|
|
719
|
+
await log(formatAligned('✅', `${argv.tool.toUpperCase()} resume completed:`, 'Checking if PR is now mergeable...'));
|
|
720
|
+
} else if (isUsageLimitReached(resumeResult)) {
|
|
721
|
+
// Hit the limit again immediately after resume — store for next outer iteration
|
|
722
|
+
await log(formatAligned('⚠️', 'Usage limit hit again after resume', 'Will retry in next check cycle', 2));
|
|
723
|
+
} else {
|
|
724
|
+
// Resume failed for a non-limit reason — stop the loop
|
|
725
|
+
await log('');
|
|
726
|
+
await log(formatAligned('❌', `${argv.tool.toUpperCase()} RESUME FAILED`, ''));
|
|
727
|
+
await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed after limit reset', 2));
|
|
728
|
+
return { success: false, reason: 'tool_failure_after_resume', latestSessionId, latestAnthropicCost };
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
// No session ID available — cannot resume, restart fresh in next iteration
|
|
732
|
+
await log(formatAligned('⚠️', 'No session ID for resume', 'Will restart fresh in next check cycle', 2));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
lastCheckTime = new Date();
|
|
736
|
+
continue;
|
|
655
737
|
}
|
|
738
|
+
|
|
739
|
+
// Any other failure (not usage limit): stop the auto-restart loop
|
|
740
|
+
// Per reviewer feedback: non-limit failures should fail and stop attempts
|
|
741
|
+
await log('');
|
|
742
|
+
await log(formatAligned('❌', `${argv.tool.toUpperCase()} EXECUTION FAILED`, ''));
|
|
743
|
+
await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed', 2));
|
|
744
|
+
return { success: false, reason: 'tool_failure', latestSessionId, latestAnthropicCost };
|
|
656
745
|
} else {
|
|
657
|
-
// Success -
|
|
658
|
-
consecutiveApiErrors = 0;
|
|
746
|
+
// Success - capture latest session data
|
|
659
747
|
currentBackoffSeconds = watchInterval;
|
|
660
|
-
|
|
661
|
-
// Capture latest session data
|
|
662
748
|
if (toolResult.sessionId) {
|
|
663
749
|
latestSessionId = toolResult.sessionId;
|
|
664
750
|
latestAnthropicCost = toolResult.anthropicTotalCostUSD;
|
|
@@ -744,7 +830,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
744
830
|
}
|
|
745
831
|
|
|
746
832
|
// Wait for next interval
|
|
747
|
-
const actualWaitSeconds =
|
|
833
|
+
const actualWaitSeconds = currentBackoffSeconds;
|
|
748
834
|
await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
|
|
749
835
|
await log('');
|
|
750
836
|
await new Promise(resolve => setTimeout(resolve, actualWaitSeconds * 1000));
|
|
@@ -359,6 +359,18 @@ export const isApiError = toolResult => {
|
|
|
359
359
|
return errorPatterns.some(pattern => toolResult.result.includes(pattern));
|
|
360
360
|
};
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Issue #1356: Check if a tool result indicates a usage limit was reached
|
|
364
|
+
* This is separate from isApiError because usage limits return different fields
|
|
365
|
+
* (limitReached, limitResetTime) and require different handling (exit loop, not retry).
|
|
366
|
+
* @param {Object} toolResult - Tool execution result
|
|
367
|
+
* @returns {boolean}
|
|
368
|
+
*/
|
|
369
|
+
export const isUsageLimitReached = toolResult => {
|
|
370
|
+
if (!toolResult) return false;
|
|
371
|
+
return toolResult.limitReached === true;
|
|
372
|
+
};
|
|
373
|
+
|
|
362
374
|
export default {
|
|
363
375
|
checkPRMerged,
|
|
364
376
|
checkPRClosed,
|
|
@@ -369,4 +381,5 @@ export default {
|
|
|
369
381
|
buildAutoRestartInstructions,
|
|
370
382
|
buildUncommittedChangesFeedback,
|
|
371
383
|
isApiError,
|
|
384
|
+
isUsageLimitReached,
|
|
372
385
|
};
|