@oss-autopilot/core 1.3.0 → 1.5.0
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/dist/cli.bundle.cjs +65 -65
- package/dist/commands/search.js +1 -0
- package/dist/core/github-stats.d.ts +4 -0
- package/dist/core/github-stats.js +9 -0
- package/dist/core/github.d.ts +1 -1
- package/dist/core/github.js +3 -3
- package/dist/core/issue-discovery.d.ts +7 -7
- package/dist/core/issue-discovery.js +53 -20
- package/dist/core/issue-vetting.d.ts +1 -0
- package/dist/core/issue-vetting.js +21 -5
- package/dist/core/pr-monitor.js +9 -0
- package/dist/core/search-phases.js +11 -7
- package/dist/core/utils.d.ts +2 -0
- package/dist/core/utils.js +6 -2
- package/dist/formatters/json.d.ts +1 -0
- package/package.json +5 -5
package/dist/commands/search.js
CHANGED
|
@@ -34,6 +34,8 @@ export declare function fetchUserClosedPRCounts(octokit: Octokit, githubUsername
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function fetchRecentlyClosedPRs(octokit: Octokit, config: {
|
|
36
36
|
githubUsername: string;
|
|
37
|
+
excludeRepos?: string[];
|
|
38
|
+
excludeOrgs?: string[];
|
|
37
39
|
}, days?: number): Promise<ClosedPR[]>;
|
|
38
40
|
/**
|
|
39
41
|
* Fetch PRs merged in the last N days.
|
|
@@ -41,6 +43,8 @@ export declare function fetchRecentlyClosedPRs(octokit: Octokit, config: {
|
|
|
41
43
|
*/
|
|
42
44
|
export declare function fetchRecentlyMergedPRs(octokit: Octokit, config: {
|
|
43
45
|
githubUsername: string;
|
|
46
|
+
excludeRepos?: string[];
|
|
47
|
+
excludeOrgs?: string[];
|
|
44
48
|
}, days?: number): Promise<MergedPR[]>;
|
|
45
49
|
/**
|
|
46
50
|
* Fetch merged PRs since a watermark date for incremental storage.
|
|
@@ -198,6 +198,15 @@ async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
|
|
|
198
198
|
// Skip own repos
|
|
199
199
|
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
200
200
|
continue;
|
|
201
|
+
// Exclude configured repos and orgs (#792)
|
|
202
|
+
if (config.excludeRepos?.includes(repo)) {
|
|
203
|
+
debug(MODULE, `Skipping excluded repo: ${repo}`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (config.excludeOrgs?.some((org) => org.toLowerCase() === parsed.owner.toLowerCase())) {
|
|
207
|
+
debug(MODULE, `Skipping excluded org: ${parsed.owner}`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
201
210
|
results.push(mapItem(item, { owner: parsed.owner, repo, number: parsed.number }));
|
|
202
211
|
}
|
|
203
212
|
debug(MODULE, `Found ${results.length} recently ${label} PRs`);
|
package/dist/core/github.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function getRateLimitCallbacks(): {
|
|
|
24
24
|
* Returns a cached instance if the token matches, otherwise creates a new one.
|
|
25
25
|
*
|
|
26
26
|
* The client retries on primary rate limits (up to 2 retries) and
|
|
27
|
-
* secondary rate limits (
|
|
27
|
+
* secondary rate limits (up to 3 retries).
|
|
28
28
|
*
|
|
29
29
|
* @param token - GitHub personal access token
|
|
30
30
|
* @returns Authenticated Octokit instance with rate limit throttling
|
package/dist/core/github.js
CHANGED
|
@@ -31,8 +31,8 @@ export function getRateLimitCallbacks() {
|
|
|
31
31
|
onSecondaryRateLimit: (retryAfter, options, _octokit, retryCount) => {
|
|
32
32
|
const opts = options;
|
|
33
33
|
const resetAt = new Date(Date.now() + retryAfter * 1000);
|
|
34
|
-
if (retryCount <
|
|
35
|
-
warn(MODULE, `Secondary rate limit hit (retry ${retryCount + 1}/
|
|
34
|
+
if (retryCount < 3) {
|
|
35
|
+
warn(MODULE, `Secondary rate limit hit (retry ${retryCount + 1}/3, waiting ${retryAfter}s, resets at ${formatResetTime(resetAt)}) — ${opts.method} ${opts.url}`);
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
warn(MODULE, `Secondary rate limit exceeded, not retrying — ${opts.method} ${opts.url} (resets at ${formatResetTime(resetAt)})`);
|
|
@@ -45,7 +45,7 @@ export function getRateLimitCallbacks() {
|
|
|
45
45
|
* Returns a cached instance if the token matches, otherwise creates a new one.
|
|
46
46
|
*
|
|
47
47
|
* The client retries on primary rate limits (up to 2 retries) and
|
|
48
|
-
* secondary rate limits (
|
|
48
|
+
* secondary rate limits (up to 3 retries).
|
|
49
49
|
*
|
|
50
50
|
* @param token - GitHub personal access token
|
|
51
51
|
* @returns Authenticated Octokit instance with rate limit throttling
|
|
@@ -14,11 +14,11 @@ import { type IssueCandidate } from './types.js';
|
|
|
14
14
|
* Multi-phase issue discovery engine that searches GitHub for contributable issues.
|
|
15
15
|
*
|
|
16
16
|
* Search phases (in priority order):
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* 0. Repos where user has merged PRs (highest merge probability)
|
|
18
|
+
* 0.5. Preferred organizations
|
|
19
|
+
* 1. Starred repos
|
|
20
|
+
* 2. General label-filtered search
|
|
21
|
+
* 3. Actively maintained repos
|
|
22
22
|
*
|
|
23
23
|
* Each candidate is vetted for claimability and scored 0-100 for viability.
|
|
24
24
|
*/
|
|
@@ -44,8 +44,8 @@ export declare class IssueDiscovery {
|
|
|
44
44
|
getStarredReposWithRefresh(): Promise<string[]>;
|
|
45
45
|
/**
|
|
46
46
|
* Search for issues matching our criteria.
|
|
47
|
-
* Searches in priority order: merged-PR repos first (no label filter), then
|
|
48
|
-
* then general search, then actively maintained repos.
|
|
47
|
+
* Searches in priority order: merged-PR repos first (no label filter), then preferred
|
|
48
|
+
* organizations, then starred repos, then general search, then actively maintained repos.
|
|
49
49
|
* Filters out issues from low-scoring and excluded repos.
|
|
50
50
|
*
|
|
51
51
|
* @param options - Search configuration
|
|
@@ -13,7 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { getOctokit, checkRateLimit } from './github.js';
|
|
15
15
|
import { getStateManager } from './state.js';
|
|
16
|
-
import { daysBetween, getDataDir } from './utils.js';
|
|
16
|
+
import { daysBetween, getDataDir, sleep } from './utils.js';
|
|
17
17
|
import { DEFAULT_CONFIG, SCOPE_LABELS } from './types.js';
|
|
18
18
|
import { ValidationError, errorMessage, getHttpStatusCode, isRateLimitError } from './errors.js';
|
|
19
19
|
import { debug, info, warn } from './logger.js';
|
|
@@ -22,15 +22,21 @@ import { IssueVetter } from './issue-vetting.js';
|
|
|
22
22
|
import { getTopicsForCategories } from './category-mapping.js';
|
|
23
23
|
import { buildEffectiveLabels, interleaveArrays, cachedSearchIssues, filterVetAndScore, searchInRepos, searchWithChunkedLabels, } from './search-phases.js';
|
|
24
24
|
const MODULE = 'issue-discovery';
|
|
25
|
+
/** Delay between major search phases to let GitHub's rate limit window cool down. */
|
|
26
|
+
const INTER_PHASE_DELAY_MS = 2000;
|
|
27
|
+
/** If remaining search quota is below this, skip heavy phases (2, 3). */
|
|
28
|
+
const LOW_BUDGET_THRESHOLD = 20;
|
|
29
|
+
/** If remaining search quota is below this, only run Phase 0. */
|
|
30
|
+
const CRITICAL_BUDGET_THRESHOLD = 10;
|
|
25
31
|
/**
|
|
26
32
|
* Multi-phase issue discovery engine that searches GitHub for contributable issues.
|
|
27
33
|
*
|
|
28
34
|
* Search phases (in priority order):
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
35
|
+
* 0. Repos where user has merged PRs (highest merge probability)
|
|
36
|
+
* 0.5. Preferred organizations
|
|
37
|
+
* 1. Starred repos
|
|
38
|
+
* 2. General label-filtered search
|
|
39
|
+
* 3. Actively maintained repos
|
|
34
40
|
*
|
|
35
41
|
* Each candidate is vetted for claimability and scored 0-100 for viability.
|
|
36
42
|
*/
|
|
@@ -118,8 +124,8 @@ export class IssueDiscovery {
|
|
|
118
124
|
}
|
|
119
125
|
/**
|
|
120
126
|
* Search for issues matching our criteria.
|
|
121
|
-
* Searches in priority order: merged-PR repos first (no label filter), then
|
|
122
|
-
* then general search, then actively maintained repos.
|
|
127
|
+
* Searches in priority order: merged-PR repos first (no label filter), then preferred
|
|
128
|
+
* organizations, then starred repos, then general search, then actively maintained repos.
|
|
123
129
|
* Filters out issues from low-scoring and excluded repos.
|
|
124
130
|
*
|
|
125
131
|
* @param options - Search configuration
|
|
@@ -151,23 +157,31 @@ export class IssueDiscovery {
|
|
|
151
157
|
let phase0Error = null;
|
|
152
158
|
let phase1Error = null;
|
|
153
159
|
let rateLimitHitDuringSearch = false;
|
|
154
|
-
// Pre-flight rate limit check (#100)
|
|
160
|
+
// Pre-flight rate limit check (#100) — also determines adaptive phase budget
|
|
155
161
|
this.rateLimitWarning = null;
|
|
162
|
+
let searchBudget = LOW_BUDGET_THRESHOLD - 1; // conservative: below threshold to skip heavy phases
|
|
156
163
|
try {
|
|
157
164
|
const rateLimit = await checkRateLimit(this.githubToken);
|
|
165
|
+
searchBudget = rateLimit.remaining;
|
|
158
166
|
if (rateLimit.remaining < 5) {
|
|
159
167
|
const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString('en-US', { hour12: false });
|
|
160
168
|
this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
|
|
161
169
|
warn(MODULE, this.rateLimitWarning);
|
|
162
170
|
}
|
|
171
|
+
if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
|
|
172
|
+
info(MODULE, `Search budget critical (${searchBudget} remaining) — running only Phase 0`);
|
|
173
|
+
}
|
|
174
|
+
else if (searchBudget < LOW_BUDGET_THRESHOLD) {
|
|
175
|
+
info(MODULE, `Search budget low (${searchBudget} remaining) — skipping heavy phases (2, 3)`);
|
|
176
|
+
}
|
|
163
177
|
}
|
|
164
178
|
catch (error) {
|
|
165
179
|
// Fail fast on auth errors — no point searching with a bad token
|
|
166
180
|
if (getHttpStatusCode(error) === 401) {
|
|
167
181
|
throw error;
|
|
168
182
|
}
|
|
169
|
-
// Non-fatal: proceed with
|
|
170
|
-
warn(MODULE, 'Could not check rate limit:', errorMessage(error));
|
|
183
|
+
// Non-fatal: proceed with conservative budget for transient/network errors
|
|
184
|
+
warn(MODULE, 'Could not check rate limit — using conservative budget, skipping heavy phases:', errorMessage(error));
|
|
171
185
|
}
|
|
172
186
|
// Get merged-PR repos (highest merge probability)
|
|
173
187
|
const mergedPRRepos = this.stateManager.getReposWithMergedPRs();
|
|
@@ -263,9 +277,13 @@ export class IssueDiscovery {
|
|
|
263
277
|
}
|
|
264
278
|
}
|
|
265
279
|
// Phase 0.5: Search preferred organizations (explicit user preference)
|
|
280
|
+
// Skip if budget is critical — Phase 0 results are sufficient
|
|
266
281
|
let phase0_5Error = null;
|
|
267
282
|
const preferredOrgs = config.preferredOrgs ?? [];
|
|
268
|
-
if (allCandidates.length < maxResults && preferredOrgs.length > 0) {
|
|
283
|
+
if (allCandidates.length < maxResults && preferredOrgs.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD) {
|
|
284
|
+
// Inter-phase delay to let GitHub's rate limit window cool down
|
|
285
|
+
if (phase0Repos.length > 0)
|
|
286
|
+
await sleep(INTER_PHASE_DELAY_MS);
|
|
269
287
|
// Filter out orgs already covered by Phase 0 repos
|
|
270
288
|
const phase0Orgs = new Set(phase0Repos.map((r) => r.split('/')[0]?.toLowerCase()));
|
|
271
289
|
const orgsToSearch = preferredOrgs.filter((org) => !phase0Orgs.has(org.toLowerCase())).slice(0, 5);
|
|
@@ -303,7 +321,9 @@ export class IssueDiscovery {
|
|
|
303
321
|
}
|
|
304
322
|
}
|
|
305
323
|
// Phase 1: Search starred repos (filter out already-searched Phase 0 repos)
|
|
306
|
-
|
|
324
|
+
// Skip if budget is critical
|
|
325
|
+
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD) {
|
|
326
|
+
await sleep(INTER_PHASE_DELAY_MS);
|
|
307
327
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
308
328
|
if (reposToSearch.length > 0) {
|
|
309
329
|
info(MODULE, `Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
|
|
@@ -322,11 +342,13 @@ export class IssueDiscovery {
|
|
|
322
342
|
}
|
|
323
343
|
}
|
|
324
344
|
// Phase 2: General search (if still need more)
|
|
345
|
+
// Skip if budget is low — Phases 0, 0.5, 1 are cheaper and higher-value
|
|
325
346
|
// When multiple scope tiers are active, fire one query per tier and interleave
|
|
326
347
|
// results to prevent high-volume tiers (e.g., "enhancement") from drowning out
|
|
327
348
|
// beginner results.
|
|
328
349
|
let phase2Error = null;
|
|
329
|
-
if (allCandidates.length < maxResults) {
|
|
350
|
+
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD) {
|
|
351
|
+
await sleep(INTER_PHASE_DELAY_MS);
|
|
330
352
|
info(MODULE, 'Phase 2: General issue search...');
|
|
331
353
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
332
354
|
const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
|
|
@@ -390,8 +412,10 @@ export class IssueDiscovery {
|
|
|
390
412
|
allCandidates.push(...interleaved.slice(0, remainingNeeded));
|
|
391
413
|
}
|
|
392
414
|
// Phase 3: Actively maintained repos (#349)
|
|
415
|
+
// Skip if budget is low — this phase is API-heavy with broad queries
|
|
393
416
|
let phase3Error = null;
|
|
394
|
-
if (allCandidates.length < maxResults) {
|
|
417
|
+
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD) {
|
|
418
|
+
await sleep(INTER_PHASE_DELAY_MS);
|
|
395
419
|
info(MODULE, 'Phase 3: Searching actively maintained repos...');
|
|
396
420
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
397
421
|
const thirtyDaysAgo = new Date();
|
|
@@ -430,6 +454,15 @@ export class IssueDiscovery {
|
|
|
430
454
|
warn(MODULE, `Error in maintained-repo search: ${errMsg}`);
|
|
431
455
|
}
|
|
432
456
|
}
|
|
457
|
+
// Determine if phases were skipped due to budget constraints
|
|
458
|
+
const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
|
|
459
|
+
let budgetNote = '';
|
|
460
|
+
if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
|
|
461
|
+
budgetNote = ` Most search phases were skipped due to critically low API quota (${searchBudget} remaining).`;
|
|
462
|
+
}
|
|
463
|
+
else if (phasesSkippedForBudget) {
|
|
464
|
+
budgetNote = ` Some search phases were skipped due to low API quota (${searchBudget} remaining).`;
|
|
465
|
+
}
|
|
433
466
|
if (allCandidates.length === 0) {
|
|
434
467
|
const phaseErrors = [
|
|
435
468
|
phase0Error ? `Phase 0 (merged-PR repos): ${phase0Error}` : null,
|
|
@@ -439,9 +472,9 @@ export class IssueDiscovery {
|
|
|
439
472
|
phase3Error ? `Phase 3 (maintained repos): ${phase3Error}` : null,
|
|
440
473
|
].filter(Boolean);
|
|
441
474
|
const details = phaseErrors.length > 0 ? ` ${phaseErrors.join('. ')}.` : '';
|
|
442
|
-
if (rateLimitHitDuringSearch) {
|
|
475
|
+
if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
|
|
443
476
|
this.rateLimitWarning =
|
|
444
|
-
`Search returned no results due to GitHub API rate limits.${details} ` +
|
|
477
|
+
`Search returned no results due to GitHub API rate limits.${details}${budgetNote} ` +
|
|
445
478
|
`Try again after the rate limit resets.`;
|
|
446
479
|
return [];
|
|
447
480
|
}
|
|
@@ -449,10 +482,10 @@ export class IssueDiscovery {
|
|
|
449
482
|
'Try adjusting your search criteria (languages, labels) or check your network connection.');
|
|
450
483
|
}
|
|
451
484
|
// Surface rate limit warning even with partial results (#100)
|
|
452
|
-
if (rateLimitHitDuringSearch) {
|
|
485
|
+
if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
|
|
453
486
|
this.rateLimitWarning =
|
|
454
|
-
`Search results may be incomplete: GitHub API rate limits were hit during search
|
|
455
|
-
`Found ${allCandidates.length} candidate${allCandidates.length === 1 ? '' : 's'} but some search phases
|
|
487
|
+
`Search results may be incomplete: GitHub API rate limits were hit during search.${budgetNote} ` +
|
|
488
|
+
`Found ${allCandidates.length} candidate${allCandidates.length === 1 ? '' : 's'} but some search phases were limited. ` +
|
|
456
489
|
`Try again after the rate limit resets for complete results.`;
|
|
457
490
|
}
|
|
458
491
|
// Sort by priority first, then by recommendation, then by viability score
|
|
@@ -15,6 +15,7 @@ export declare class IssueVetter {
|
|
|
15
15
|
constructor(octokit: Octokit, stateManager: ReturnType<typeof getStateManager>);
|
|
16
16
|
/**
|
|
17
17
|
* Vet a specific issue — runs all checks and computes recommendation + viability score.
|
|
18
|
+
* Results are cached for 15 minutes to avoid redundant API calls on repeated searches.
|
|
18
19
|
*/
|
|
19
20
|
vetIssue(issueUrl: string): Promise<IssueCandidate>;
|
|
20
21
|
/**
|
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
* - issue-eligibility.ts — PR existence, claim detection, requirements analysis
|
|
7
7
|
* - repo-health.ts — project health, contribution guidelines
|
|
8
8
|
*/
|
|
9
|
-
import { parseGitHubUrl
|
|
9
|
+
import { parseGitHubUrl } from './utils.js';
|
|
10
10
|
import { ValidationError, errorMessage, isRateLimitError } from './errors.js';
|
|
11
|
-
import { warn } from './logger.js';
|
|
11
|
+
import { debug, warn } from './logger.js';
|
|
12
12
|
import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
|
|
13
13
|
import { repoBelongsToCategory } from './category-mapping.js';
|
|
14
14
|
import { checkNoExistingPR, checkNotClaimed, checkUserMergedPRsInRepo, analyzeRequirements, } from './issue-eligibility.js';
|
|
15
15
|
import { checkProjectHealth, fetchContributionGuidelines } from './repo-health.js';
|
|
16
|
+
import { getHttpCache } from './http-cache.js';
|
|
16
17
|
const MODULE = 'issue-vetting';
|
|
17
|
-
|
|
18
|
+
/** Vetting concurrency: kept low to reduce burst pressure on GitHub's secondary rate limit. */
|
|
19
|
+
const MAX_CONCURRENT_VETTING = 3;
|
|
20
|
+
/** TTL for cached vetting results (15 minutes). Kept short so config changes take effect quickly. */
|
|
21
|
+
const VETTING_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
18
22
|
export class IssueVetter {
|
|
19
23
|
octokit;
|
|
20
24
|
stateManager;
|
|
@@ -24,8 +28,17 @@ export class IssueVetter {
|
|
|
24
28
|
}
|
|
25
29
|
/**
|
|
26
30
|
* Vet a specific issue — runs all checks and computes recommendation + viability score.
|
|
31
|
+
* Results are cached for 15 minutes to avoid redundant API calls on repeated searches.
|
|
27
32
|
*/
|
|
28
33
|
async vetIssue(issueUrl) {
|
|
34
|
+
// Check vetting cache first — avoids ~6+ API calls per issue
|
|
35
|
+
const cache = getHttpCache();
|
|
36
|
+
const cacheKey = `vet:${issueUrl}`;
|
|
37
|
+
const cached = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
|
|
38
|
+
if (cached && typeof cached === 'object' && 'issue' in cached && 'viabilityScore' in cached) {
|
|
39
|
+
debug(MODULE, `Vetting cache hit for ${issueUrl}`);
|
|
40
|
+
return cached;
|
|
41
|
+
}
|
|
29
42
|
// Parse URL
|
|
30
43
|
const parsed = parseGitHubUrl(issueUrl);
|
|
31
44
|
if (!parsed || parsed.type !== 'issues') {
|
|
@@ -207,7 +220,7 @@ export class IssueVetter {
|
|
|
207
220
|
else if (starredRepos.includes(repoFullName)) {
|
|
208
221
|
searchPriority = 'starred';
|
|
209
222
|
}
|
|
210
|
-
|
|
223
|
+
const result = {
|
|
211
224
|
issue: trackedIssue,
|
|
212
225
|
vettingResult,
|
|
213
226
|
projectHealth,
|
|
@@ -217,6 +230,9 @@ export class IssueVetter {
|
|
|
217
230
|
viabilityScore,
|
|
218
231
|
searchPriority,
|
|
219
232
|
};
|
|
233
|
+
// Cache the vetting result to avoid redundant API calls on repeated searches
|
|
234
|
+
cache.set(cacheKey, '', result);
|
|
235
|
+
return result;
|
|
220
236
|
}
|
|
221
237
|
/**
|
|
222
238
|
* Vet multiple issues in parallel with concurrency limit
|
|
@@ -251,7 +267,7 @@ export class IssueVetter {
|
|
|
251
267
|
.finally(() => pending.delete(url));
|
|
252
268
|
pending.set(url, task);
|
|
253
269
|
// Limit concurrency — wait for at least one to complete before launching more
|
|
254
|
-
if (pending.size >=
|
|
270
|
+
if (pending.size >= MAX_CONCURRENT_VETTING) {
|
|
255
271
|
await Promise.race(pending.values());
|
|
256
272
|
}
|
|
257
273
|
}
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -124,6 +124,15 @@ export class PRMonitor {
|
|
|
124
124
|
}
|
|
125
125
|
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
126
126
|
return false;
|
|
127
|
+
// Exclude configured repos and orgs (#792)
|
|
128
|
+
if (config.excludeRepos.includes(`${parsed.owner}/${parsed.repo}`)) {
|
|
129
|
+
debug('pr-monitor', `Skipping excluded repo: ${parsed.owner}/${parsed.repo}`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (config.excludeOrgs?.some((org) => org.toLowerCase() === parsed.owner.toLowerCase())) {
|
|
133
|
+
debug('pr-monitor', `Skipping excluded org: ${parsed.owner}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
127
136
|
return true;
|
|
128
137
|
});
|
|
129
138
|
debug('pr-monitor', `Filtered to ${filteredItems.length} PRs after excluding own repos`);
|
|
@@ -9,16 +9,14 @@ import { errorMessage, isRateLimitError } from './errors.js';
|
|
|
9
9
|
import { debug, warn } from './logger.js';
|
|
10
10
|
import { getHttpCache, cachedTimeBased } from './http-cache.js';
|
|
11
11
|
import { detectLabelFarmingRepos } from './issue-filtering.js';
|
|
12
|
+
import { sleep } from './utils.js';
|
|
12
13
|
const MODULE = 'search-phases';
|
|
13
14
|
/** GitHub Search API enforces a max of 5 AND/OR/NOT operators per query. */
|
|
14
15
|
export const GITHUB_MAX_BOOLEAN_OPS = 5;
|
|
15
|
-
/**
|
|
16
|
-
const INTER_QUERY_DELAY_MS =
|
|
16
|
+
/** Delay between search API calls to avoid GitHub's secondary rate limit (~30 req/min). */
|
|
17
|
+
const INTER_QUERY_DELAY_MS = 1500;
|
|
17
18
|
/** Batch size for repo queries. 3 repos = 2 OR operators, leaving room for labels. */
|
|
18
19
|
const BATCH_SIZE = 3;
|
|
19
|
-
function sleep(ms) {
|
|
20
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
-
}
|
|
22
20
|
/**
|
|
23
21
|
* Chunk labels into groups that fit within the operator budget.
|
|
24
22
|
* N labels require N-1 OR operators, so maxPerChunk = budget + 1.
|
|
@@ -189,9 +187,13 @@ export async function searchInRepos(octokit, vetter, repos, baseQualifiers, labe
|
|
|
189
187
|
const batches = batchRepos(repos, BATCH_SIZE);
|
|
190
188
|
let failedBatches = 0;
|
|
191
189
|
let rateLimitFailures = 0;
|
|
192
|
-
for (
|
|
190
|
+
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
191
|
+
const batch = batches[batchIdx];
|
|
193
192
|
if (candidates.length >= maxResults)
|
|
194
193
|
break;
|
|
194
|
+
// Delay between batches to avoid secondary rate limits
|
|
195
|
+
if (batchIdx > 0)
|
|
196
|
+
await sleep(INTER_QUERY_DELAY_MS);
|
|
195
197
|
try {
|
|
196
198
|
const repoFilter = batch.map((r) => `repo:${r}`).join(' OR ');
|
|
197
199
|
const repoOps = batch.length - 1;
|
|
@@ -200,8 +202,10 @@ export async function searchInRepos(octokit, vetter, repos, baseQualifiers, labe
|
|
|
200
202
|
if (allItems.length > 0) {
|
|
201
203
|
const filtered = filterFn(allItems);
|
|
202
204
|
const remainingNeeded = maxResults - candidates.length;
|
|
203
|
-
const { candidates: vetted } = await vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((i) => i.html_url), remainingNeeded, priority);
|
|
205
|
+
const { candidates: vetted, rateLimitHit: vetRateLimitHit } = await vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((i) => i.html_url), remainingNeeded, priority);
|
|
204
206
|
candidates.push(...vetted);
|
|
207
|
+
if (vetRateLimitHit)
|
|
208
|
+
rateLimitFailures++;
|
|
205
209
|
}
|
|
206
210
|
}
|
|
207
211
|
catch (error) {
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/** Default concurrency limit for parallel GitHub API requests. */
|
|
5
5
|
export declare const DEFAULT_CONCURRENCY = 5;
|
|
6
|
+
/** Async sleep — exported for mockability in tests. */
|
|
7
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
6
8
|
/**
|
|
7
9
|
* Returns the oss-autopilot data directory path, creating it if it does not exist.
|
|
8
10
|
*
|
package/dist/core/utils.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared utility functions
|
|
3
3
|
*/
|
|
4
|
-
/** Default concurrency limit for parallel GitHub API requests. */
|
|
5
|
-
export const DEFAULT_CONCURRENCY = 5;
|
|
6
4
|
import * as fs from 'fs';
|
|
7
5
|
import * as path from 'path';
|
|
8
6
|
import * as os from 'os';
|
|
9
7
|
import { execFileSync, execFile } from 'child_process';
|
|
10
8
|
import { ConfigurationError } from './errors.js';
|
|
11
9
|
import { debug } from './logger.js';
|
|
10
|
+
/** Default concurrency limit for parallel GitHub API requests. */
|
|
11
|
+
export const DEFAULT_CONCURRENCY = 5;
|
|
12
|
+
/** Async sleep — exported for mockability in tests. */
|
|
13
|
+
export function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
12
16
|
const MODULE = 'utils';
|
|
13
17
|
// Cached GitHub token (fetched once per session)
|
|
14
18
|
let cachedGitHubToken = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-autopilot/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "CLI and core library for managing open source contributions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"zod": "^4.3.6"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@types/node": "^25.
|
|
58
|
-
"@vitest/coverage-v8": "^4.0
|
|
59
|
-
"esbuild": "^0.27.
|
|
57
|
+
"@types/node": "^25.5.0",
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
59
|
+
"esbuild": "^0.27.4",
|
|
60
60
|
"tsx": "^4.21.0",
|
|
61
61
|
"typedoc": "^0.28.17",
|
|
62
62
|
"typescript": "^5.9.3",
|
|
63
|
-
"vitest": "^4.0
|
|
63
|
+
"vitest": "^4.1.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsc",
|