@link-assistant/hive-mind 1.69.8 → 1.69.10
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 +26 -0
- package/CLAUDE.md +5 -0
- package/package.json +1 -1
- package/src/github-rate-limit.lib.mjs +126 -1
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.mjs +5 -1
- package/src/solve.repo-setup.lib.mjs +32 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.69.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7d58938: feat: add opt-in GitHub API rate-limit usage logging
|
|
8
|
+
|
|
9
|
+
Adds optional logging of current GitHub API rate-limit usage through the centralized `gh` retry wrapper so every wrapped GitHub CLI call can report quota usage while debugging.
|
|
10
|
+
|
|
11
|
+
Features:
|
|
12
|
+
- Disabled by default for backward compatibility
|
|
13
|
+
- Enable with `--github-rate-limits-logging` when debugging API usage
|
|
14
|
+
- Logs current `core`, `graphql`, and `search` rate-limit buckets after each centralized wrapped `gh` attempt
|
|
15
|
+
- Keeps the logging probe non-fatal so quota logging cannot break solve workflows
|
|
16
|
+
|
|
17
|
+
Example output:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
📊 GitHub rate limits after $gh (gh api repos): core: 780/5000 used (+29 since last check), 4220 remaining, resets 2026-05-12T10:30:00.000Z; graphql: 10/5000 used (no change), 4990 remaining, resets 2026-05-12T10:30:00.000Z
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 1.69.9
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 9d04a2f: Detect empty repositories before branch creation when Git reports an unborn branch name.
|
|
28
|
+
|
|
3
29
|
## 1.69.8
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/CLAUDE.md
ADDED
package/package.json
CHANGED
|
@@ -24,8 +24,16 @@ import { limitReset, retryLimits } from './config.lib.mjs';
|
|
|
24
24
|
|
|
25
25
|
const exec = promisify(execCb);
|
|
26
26
|
|
|
27
|
+
const GITHUB_RATE_LIMIT_USAGE_RESOURCES = ['core', 'graphql', 'search'];
|
|
27
28
|
const RATE_LIMIT_PATTERNS = ['api rate limit exceeded', 'rate limit exceeded', 'you have exceeded a secondary rate limit', 'secondary rate limit', 'abuse detection', 'was submitted too quickly'];
|
|
28
29
|
|
|
30
|
+
const githubRateLimitLogging = {
|
|
31
|
+
enabled: false,
|
|
32
|
+
log: null,
|
|
33
|
+
fetchUsage: null,
|
|
34
|
+
lastUsageByResource: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
29
37
|
/**
|
|
30
38
|
* Pull every plausible string out of a thrown error/result so pattern matches
|
|
31
39
|
* survive whatever shape the upstream caller gave us (Error, exec result with
|
|
@@ -120,6 +128,116 @@ export const fetchNextRateLimitReset = async () => {
|
|
|
120
128
|
}
|
|
121
129
|
};
|
|
122
130
|
|
|
131
|
+
const toFiniteNumber = value => {
|
|
132
|
+
const number = Number(value);
|
|
133
|
+
return Number.isFinite(number) ? number : null;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const normalizeRateLimitUsageEntry = (resource, entry) => {
|
|
137
|
+
if (!resource || !entry) return null;
|
|
138
|
+
const limit = toFiniteNumber(entry.limit);
|
|
139
|
+
const used = toFiniteNumber(entry.used);
|
|
140
|
+
const remaining = toFiniteNumber(entry.remaining);
|
|
141
|
+
const reset = toFiniteNumber(entry.reset);
|
|
142
|
+
if (limit === null || used === null || remaining === null) return null;
|
|
143
|
+
return {
|
|
144
|
+
resource,
|
|
145
|
+
limit,
|
|
146
|
+
used,
|
|
147
|
+
remaining,
|
|
148
|
+
reset,
|
|
149
|
+
resetDate: reset === null ? null : new Date(reset * 1000),
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fetch the current GitHub API usage buckets we commonly exercise via `gh`.
|
|
155
|
+
* This intentionally calls `gh api rate_limit` directly so the logging probe
|
|
156
|
+
* does not recursively pass through the retry/logging wrapper it supports.
|
|
157
|
+
*
|
|
158
|
+
* @returns {Promise<Array<{resource: string, limit: number, used: number, remaining: number, reset: number|null, resetDate: Date|null}>>}
|
|
159
|
+
*/
|
|
160
|
+
export const fetchGitHubRateLimitUsage = async () => {
|
|
161
|
+
try {
|
|
162
|
+
// eslint-disable-next-line gh-rate-limit/no-direct-gh-exec -- this IS the centralized rate-limit helper; routing through itself would recurse.
|
|
163
|
+
const { stdout } = await exec('gh api rate_limit');
|
|
164
|
+
const data = JSON.parse(stdout);
|
|
165
|
+
const resources = data?.resources || {};
|
|
166
|
+
return GITHUB_RATE_LIMIT_USAGE_RESOURCES.map(resource => normalizeRateLimitUsageEntry(resource, resources[resource])).filter(Boolean);
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Enable optional debug logging of actual GitHub API quota usage after each
|
|
174
|
+
* centralized `gh` wrapper attempt. Disabled by default for backward
|
|
175
|
+
* compatibility and to avoid extra `gh api rate_limit` probes in normal runs.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} [options]
|
|
178
|
+
* @param {boolean} [options.enabled=false]
|
|
179
|
+
* @param {(msg: string, options?: object) => Promise<void>|void} [options.log]
|
|
180
|
+
* @param {() => Promise<Array<object>>} [options.fetchUsage] - injectable for tests.
|
|
181
|
+
*/
|
|
182
|
+
export const configureGitHubRateLimitLogging = ({ enabled = false, log = null, fetchUsage = null } = {}) => {
|
|
183
|
+
githubRateLimitLogging.enabled = enabled === true;
|
|
184
|
+
githubRateLimitLogging.log = typeof log === 'function' ? log : null;
|
|
185
|
+
githubRateLimitLogging.fetchUsage = typeof fetchUsage === 'function' ? fetchUsage : fetchGitHubRateLimitUsage;
|
|
186
|
+
githubRateLimitLogging.lastUsageByResource = null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const isGitHubRateLimitLoggingEnabled = () => githubRateLimitLogging.enabled;
|
|
190
|
+
|
|
191
|
+
const formatUsageReset = entry => {
|
|
192
|
+
if (!(entry.resetDate instanceof Date) || Number.isNaN(entry.resetDate.getTime())) return '';
|
|
193
|
+
return `, resets ${entry.resetDate.toISOString()}`;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const formatRateLimitUsageEntry = entry => {
|
|
197
|
+
const previous = githubRateLimitLogging.lastUsageByResource?.[entry.resource];
|
|
198
|
+
let deltaText = '';
|
|
199
|
+
if (previous && Number.isFinite(previous.used)) {
|
|
200
|
+
const delta = entry.used - previous.used;
|
|
201
|
+
if (delta > 0) {
|
|
202
|
+
deltaText = ` (+${delta} since last check)`;
|
|
203
|
+
} else if (delta === 0) {
|
|
204
|
+
deltaText = ' (no change)';
|
|
205
|
+
} else {
|
|
206
|
+
deltaText = ' (usage reset since last check)';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return `${entry.resource}: ${entry.used}/${entry.limit} used${deltaText}, ${entry.remaining} remaining${formatUsageReset(entry)}`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const safelyLogRateLimitUsage = async (logger, message, options) => {
|
|
213
|
+
try {
|
|
214
|
+
await Promise.resolve(logger(message, options));
|
|
215
|
+
} catch {
|
|
216
|
+
// Debug logging must never replace the original gh result or error.
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const logGitHubRateLimitUsage = async ({ label = 'gh' } = {}) => {
|
|
221
|
+
if (!githubRateLimitLogging.enabled) return [];
|
|
222
|
+
const logger = githubRateLimitLogging.log || (msg => console.warn(msg));
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const rawUsage = await githubRateLimitLogging.fetchUsage();
|
|
226
|
+
const usage = (Array.isArray(rawUsage) ? rawUsage : []).map(entry => normalizeRateLimitUsageEntry(entry.resource || entry.name, entry)).filter(Boolean);
|
|
227
|
+
if (usage.length === 0) return [];
|
|
228
|
+
|
|
229
|
+
const details = usage.map(formatRateLimitUsageEntry).join('; ');
|
|
230
|
+
await safelyLogRateLimitUsage(logger, `📊 GitHub rate limits after ${label}: ${details}`);
|
|
231
|
+
githubRateLimitLogging.lastUsageByResource = Object.fromEntries(usage.map(entry => [entry.resource, entry]));
|
|
232
|
+
return usage;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (global.verboseMode) {
|
|
235
|
+
await safelyLogRateLimitUsage(logger, `⚠️ GitHub rate-limit logging failed after ${label}: ${error.message}`, { verbose: true });
|
|
236
|
+
}
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
123
241
|
/**
|
|
124
242
|
* Compute the absolute wait deadline that satisfies issue #1726:
|
|
125
243
|
* reset + bufferMs (default 10 min) + random(0..jitterMs) (default 0-5 min)
|
|
@@ -226,8 +344,11 @@ export const ghWithRateLimitRetry = async (fn, options = {}) => {
|
|
|
226
344
|
|
|
227
345
|
for (let i = 0; i < hardCap; i++) {
|
|
228
346
|
try {
|
|
229
|
-
|
|
347
|
+
const result = await fn();
|
|
348
|
+
await logGitHubRateLimitUsage({ label });
|
|
349
|
+
return result;
|
|
230
350
|
} catch (error) {
|
|
351
|
+
await logGitHubRateLimitUsage({ label });
|
|
231
352
|
lastError = error;
|
|
232
353
|
|
|
233
354
|
if (isRateLimitError(error)) {
|
|
@@ -324,6 +445,10 @@ export default {
|
|
|
324
445
|
isTransientNetworkError,
|
|
325
446
|
parseRateLimitReset,
|
|
326
447
|
fetchNextRateLimitReset,
|
|
448
|
+
fetchGitHubRateLimitUsage,
|
|
449
|
+
configureGitHubRateLimitLogging,
|
|
450
|
+
isGitHubRateLimitLoggingEnabled,
|
|
451
|
+
logGitHubRateLimitUsage,
|
|
327
452
|
computeRateLimitWait,
|
|
328
453
|
ghWithRateLimitRetry,
|
|
329
454
|
execGhWithRetry,
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -461,6 +461,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
461
461
|
description: 'Include prompt to check related/sibling pull requests when studying related work. Enabled by default, use --no-prompt-check-sibling-pull-requests to disable.',
|
|
462
462
|
default: true,
|
|
463
463
|
},
|
|
464
|
+
'github-rate-limits-logging': {
|
|
465
|
+
type: 'boolean',
|
|
466
|
+
description: 'Log GitHub API rate-limit usage after each centralized gh command retry wrapper call. Disabled by default; use --github-rate-limits-logging to enable.',
|
|
467
|
+
default: false,
|
|
468
|
+
},
|
|
464
469
|
'prompt-experiments-folder': {
|
|
465
470
|
type: 'string',
|
|
466
471
|
description: 'Path to experiments folder used in system prompt. Set to empty string to disable experiments folder prompt. Default: ./experiments',
|
package/src/solve.mjs
CHANGED
|
@@ -8,7 +8,7 @@ await handleSolveEarlyExit(earlyArgs);
|
|
|
8
8
|
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
|
|
9
9
|
globalThis.use = use;
|
|
10
10
|
const { $: __rawDollar$ } = await use('command-stream');
|
|
11
|
-
const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
|
|
11
|
+
const { configureGitHubRateLimitLogging, wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
|
|
12
12
|
const $ = wrapDollarWithGhRetry(__rawDollar$);
|
|
13
13
|
const config = await import('./solve.config.lib.mjs');
|
|
14
14
|
const { initializeConfig, parseArguments } = config;
|
|
@@ -86,6 +86,10 @@ global.verboseMode = argv.verbose;
|
|
|
86
86
|
|
|
87
87
|
setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
|
|
88
88
|
setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
|
|
89
|
+
configureGitHubRateLimitLogging({
|
|
90
|
+
enabled: argv.githubRateLimitsLogging === true,
|
|
91
|
+
log,
|
|
92
|
+
});
|
|
89
93
|
|
|
90
94
|
// Early logs go to cwd; custom log dir takes effect after argv is parsed
|
|
91
95
|
// Conditionally import tool-specific functions after argv is parsed
|
|
@@ -71,12 +71,10 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
let defaultBranch = defaultBranchResult.stdout.toString().trim();
|
|
74
|
-
|
|
75
|
-
// Repository is likely empty (no commits) - detect and handle
|
|
76
|
-
const isEmptyRepo = await detectEmptyRepository(tempDir, $);
|
|
74
|
+
const isEmptyRepo = await detectEmptyRepository(tempDir, $);
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
if (isEmptyRepo) {
|
|
77
|
+
if (argv && argv.autoInitRepository && owner && repo) {
|
|
80
78
|
await log('');
|
|
81
79
|
await log(`${formatAligned('⚠️', 'EMPTY REPOSITORY', 'detected')}`, { level: 'warn' });
|
|
82
80
|
await log(`${formatAligned('', '', `Repository ${owner}/${repo} contains no commits`)}`);
|
|
@@ -126,7 +124,6 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
|
|
|
126
124
|
await log(`${formatAligned('✅', 'Repository initialized:', `Now on branch ${defaultBranch}`)}`);
|
|
127
125
|
await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
|
|
128
126
|
} else {
|
|
129
|
-
// Auto-init failed - provide helpful message with --auto-init-repository context
|
|
130
127
|
await log('');
|
|
131
128
|
await log(`${formatAligned('❌', 'AUTO-INIT FAILED', '')}`, { level: 'error' });
|
|
132
129
|
await log('');
|
|
@@ -149,8 +146,7 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
|
|
|
149
146
|
|
|
150
147
|
throw new Error('Empty repository auto-initialization failed');
|
|
151
148
|
}
|
|
152
|
-
} else
|
|
153
|
-
// Empty repo detected but --auto-init-repository is not enabled
|
|
149
|
+
} else {
|
|
154
150
|
await log('');
|
|
155
151
|
await log(`${formatAligned('❌', 'EMPTY REPOSITORY DETECTED', '')}`, { level: 'error' });
|
|
156
152
|
await log('');
|
|
@@ -170,25 +166,24 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
|
|
|
170
166
|
await tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ });
|
|
171
167
|
|
|
172
168
|
throw new Error('Empty repository detected - use --auto-init-repository to initialize');
|
|
173
|
-
} else {
|
|
174
|
-
// Not an empty repo, some other issue with branch detection
|
|
175
|
-
await log('');
|
|
176
|
-
await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' });
|
|
177
|
-
await log('');
|
|
178
|
-
await log(' 🔍 What happened:');
|
|
179
|
-
await log(" Unable to determine the repository's default branch.");
|
|
180
|
-
await log('');
|
|
181
|
-
await log(' 💡 This might mean:');
|
|
182
|
-
await log(' • Unusual repository configuration');
|
|
183
|
-
await log(' • Git command issues');
|
|
184
|
-
await log('');
|
|
185
|
-
await log(' 🔧 How to fix:');
|
|
186
|
-
await log(' 1. Check repository status');
|
|
187
|
-
await log(` 2. Verify locally: cd ${tempDir} && git branch`);
|
|
188
|
-
await log(` 3. Check remote: cd ${tempDir} && git branch -r`);
|
|
189
|
-
await log('');
|
|
190
|
-
throw new Error('Default branch detection failed');
|
|
191
169
|
}
|
|
170
|
+
} else if (!defaultBranch) {
|
|
171
|
+
await log('');
|
|
172
|
+
await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' });
|
|
173
|
+
await log('');
|
|
174
|
+
await log(' 🔍 What happened:');
|
|
175
|
+
await log(" Unable to determine the repository's default branch.");
|
|
176
|
+
await log('');
|
|
177
|
+
await log(' 💡 This might mean:');
|
|
178
|
+
await log(' • Unusual repository configuration');
|
|
179
|
+
await log(' • Git command issues');
|
|
180
|
+
await log('');
|
|
181
|
+
await log(' 🔧 How to fix:');
|
|
182
|
+
await log(' 1. Check repository status');
|
|
183
|
+
await log(` 2. Verify locally: cd ${tempDir} && git branch`);
|
|
184
|
+
await log(` 3. Check remote: cd ${tempDir} && git branch -r`);
|
|
185
|
+
await log('');
|
|
186
|
+
throw new Error('Default branch detection failed');
|
|
192
187
|
} else {
|
|
193
188
|
await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
|
|
194
189
|
}
|
|
@@ -267,16 +262,19 @@ Thank you!`;
|
|
|
267
262
|
*/
|
|
268
263
|
async function detectEmptyRepository(tempDir, $) {
|
|
269
264
|
// Check if there are any commits in the repository
|
|
270
|
-
const logResult = await $({ cwd: tempDir })`git rev-parse HEAD 2>&1`;
|
|
271
|
-
if (logResult.code
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
265
|
+
const logResult = await $({ cwd: tempDir })`git rev-parse --verify HEAD 2>&1`;
|
|
266
|
+
if (logResult.code === 0) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// git rev-parse HEAD fails when there are no commits
|
|
271
|
+
const output = (logResult.stdout || logResult.stderr || '').toString();
|
|
272
|
+
if (output.includes('unknown revision') || output.includes('ambiguous argument') || output.includes('bad default revision') || output.includes('does not have any commits') || output.includes('Needed a single revision')) {
|
|
273
|
+
return true;
|
|
277
274
|
}
|
|
278
275
|
|
|
279
|
-
//
|
|
276
|
+
// Fall back to remote branch absence for Git versions with different
|
|
277
|
+
// no-commit messages, but only after HEAD lookup failed.
|
|
280
278
|
const remoteBranchResult = await $({ cwd: tempDir })`git branch -r`;
|
|
281
279
|
if (remoteBranchResult.code === 0) {
|
|
282
280
|
const branches = remoteBranchResult.stdout.toString().trim();
|