@oss-scout/mcp 0.3.0 → 0.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.
@@ -35742,6 +35742,11 @@ var StoredClosedPRSchema = external_exports3.object({
35742
35742
  title: external_exports3.string(),
35743
35743
  closedAt: external_exports3.string()
35744
35744
  });
35745
+ var StoredOpenPRSchema = external_exports3.object({
35746
+ url: external_exports3.string(),
35747
+ title: external_exports3.string(),
35748
+ openedAt: external_exports3.string()
35749
+ });
35745
35750
  var ContributionGuidelinesSchema = external_exports3.object({
35746
35751
  branchNamingConvention: external_exports3.string().optional(),
35747
35752
  commitMessageFormat: external_exports3.string().optional(),
@@ -35817,8 +35822,11 @@ var ScoutPreferencesSchema = external_exports3.object({
35817
35822
  maxIssueAgeDays: external_exports3.number().default(90),
35818
35823
  includeDocIssues: external_exports3.boolean().default(true),
35819
35824
  minRepoScoreThreshold: external_exports3.number().default(4),
35825
+ interPhaseDelayMs: external_exports3.number().min(0).max(12e4).default(3e4),
35820
35826
  persistence: PersistenceModeSchema.default("local"),
35821
- defaultStrategy: external_exports3.array(SearchStrategySchema).optional()
35827
+ defaultStrategy: external_exports3.array(SearchStrategySchema).optional(),
35828
+ broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
35829
+ skipBroadWhenSufficientResults: external_exports3.number().int().min(0).max(100).default(15)
35822
35830
  });
35823
35831
  var ScoutStateSchema = external_exports3.object({
35824
35832
  version: external_exports3.literal(1),
@@ -35828,6 +35836,7 @@ var ScoutStateSchema = external_exports3.object({
35828
35836
  starredReposLastFetched: external_exports3.string().optional(),
35829
35837
  mergedPRs: external_exports3.array(StoredMergedPRSchema).default([]),
35830
35838
  closedPRs: external_exports3.array(StoredClosedPRSchema).default([]),
35839
+ openPRs: external_exports3.array(StoredOpenPRSchema).default([]),
35831
35840
  savedResults: external_exports3.array(SavedCandidateSchema).default([]),
35832
35841
  skippedIssues: external_exports3.array(SkippedIssueSchema).default([]),
35833
35842
  lastSearchAt: external_exports3.string().optional(),
@@ -36833,7 +36842,6 @@ var IssueVetter = class {
36833
36842
  var MODULE8 = "search-phases";
36834
36843
  var GITHUB_MAX_BOOLEAN_OPS = 5;
36835
36844
  var INTER_QUERY_DELAY_MS = 2e3;
36836
- var BATCH_SIZE = 3;
36837
36845
  function chunkLabels(labels, reservedOps = 0) {
36838
36846
  const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
36839
36847
  if (maxPerChunk < 1) {
@@ -36879,26 +36887,138 @@ function interleaveArrays(arrays) {
36879
36887
  }
36880
36888
  return result;
36881
36889
  }
36882
- function batchRepos(repos, batchSize) {
36883
- const batches = [];
36884
- for (let i = 0; i < repos.length; i += batchSize) {
36885
- batches.push(repos.slice(i, i + batchSize));
36886
- }
36887
- return batches;
36888
- }
36889
36890
  var SEARCH_CACHE_TTL_MS = 15 * 60 * 1e3;
36890
36891
  async function cachedSearchIssues(octokit, params) {
36891
36892
  const cacheKey = `search:${params.q}:${params.sort}:${params.order}:${params.per_page}`;
36892
- return cachedTimeBased(getHttpCache(), cacheKey, SEARCH_CACHE_TTL_MS, async () => {
36893
- const tracker = getSearchBudgetTracker();
36894
- await tracker.waitForBudget();
36893
+ const cache = getHttpCache();
36894
+ const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
36895
+ if (cached2) {
36896
+ debug(MODULE8, `Search cache hit for query`);
36897
+ return cached2;
36898
+ }
36899
+ const tracker = getSearchBudgetTracker();
36900
+ await tracker.waitForBudget();
36901
+ let data;
36902
+ try {
36903
+ const response = await octokit.search.issuesAndPullRequests(params);
36904
+ data = response.data;
36905
+ } finally {
36906
+ tracker.recordCall();
36907
+ }
36908
+ if (data.items.length > 0) {
36909
+ cache.set(cacheKey, "", data);
36910
+ } else {
36911
+ debug(MODULE8, `Skipping cache for empty search result (possible rate limit artifact)`);
36912
+ }
36913
+ return data;
36914
+ }
36915
+ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResults) {
36916
+ const items = [];
36917
+ for (const repoFullName of repos) {
36918
+ if (items.length >= maxResults * 3)
36919
+ break;
36920
+ const [owner, repo] = repoFullName.split("/");
36921
+ if (!owner || !repo)
36922
+ continue;
36895
36923
  try {
36896
- const { data } = await octokit.search.issuesAndPullRequests(params);
36897
- return data;
36898
- } finally {
36899
- tracker.recordCall();
36924
+ const { data: repoData } = await octokit.repos.get({ owner, repo });
36925
+ if (!repoData.pushed_at)
36926
+ continue;
36927
+ const pushedAt = new Date(repoData.pushed_at);
36928
+ const thirtyDaysAgo = /* @__PURE__ */ new Date();
36929
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
36930
+ if (pushedAt < thirtyDaysAgo)
36931
+ continue;
36932
+ if ((repoData.stargazers_count ?? 0) < minStars)
36933
+ continue;
36934
+ if (repoData.archived)
36935
+ continue;
36936
+ const { data: issues } = await octokit.issues.listForRepo({
36937
+ owner,
36938
+ repo,
36939
+ state: "open",
36940
+ sort: "created",
36941
+ direction: "desc",
36942
+ per_page: 5
36943
+ });
36944
+ const realIssues = issues.filter((i) => !i.pull_request && !i.assignee);
36945
+ for (const issue2 of realIssues) {
36946
+ items.push({
36947
+ html_url: issue2.html_url,
36948
+ repository_url: `https://api.github.com/repos/${repoFullName}`,
36949
+ updated_at: issue2.updated_at ?? "",
36950
+ title: issue2.title,
36951
+ labels: issue2.labels
36952
+ });
36953
+ }
36954
+ await sleep(INTER_QUERY_DELAY_MS);
36955
+ } catch (error48) {
36956
+ if (getHttpStatusCode(error48) === 401)
36957
+ throw error48;
36958
+ if (isRateLimitError(error48)) {
36959
+ warn(MODULE8, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
36960
+ break;
36961
+ }
36962
+ warn(MODULE8, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
36900
36963
  }
36901
- });
36964
+ }
36965
+ return items;
36966
+ }
36967
+ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn) {
36968
+ const candidates = [];
36969
+ let failedRepos = 0;
36970
+ let rateLimitFailures = 0;
36971
+ for (let i = 0; i < repos.length; i++) {
36972
+ if (candidates.length >= maxResults)
36973
+ break;
36974
+ if (i > 0)
36975
+ await sleep(INTER_QUERY_DELAY_MS);
36976
+ const repoFullName = repos[i];
36977
+ const [owner, repo] = repoFullName.split("/");
36978
+ try {
36979
+ const response = await octokit.issues.listForRepo({
36980
+ owner,
36981
+ repo,
36982
+ state: "open",
36983
+ sort: "created",
36984
+ direction: "desc",
36985
+ per_page: 5,
36986
+ ...labels.length > 0 ? { labels: labels.join(",") } : {}
36987
+ });
36988
+ const issuesOnly = response.data.filter((item) => !("pull_request" in item) && !item.assignee);
36989
+ const mapped = issuesOnly.map((issue2) => ({
36990
+ html_url: issue2.html_url,
36991
+ repository_url: `https://api.github.com/repos/${repoFullName}`,
36992
+ updated_at: issue2.updated_at ?? "",
36993
+ title: issue2.title,
36994
+ labels: issue2.labels
36995
+ }));
36996
+ if (mapped.length > 0) {
36997
+ const filtered = filterFn(mapped);
36998
+ if (filtered.length > 0) {
36999
+ const remainingNeeded = maxResults - candidates.length;
37000
+ const { candidates: vetted, rateLimitHit: vetRateLimitHit } = await vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((item) => item.html_url), remainingNeeded, priority);
37001
+ candidates.push(...vetted);
37002
+ if (vetRateLimitHit)
37003
+ rateLimitFailures++;
37004
+ }
37005
+ }
37006
+ } catch (error48) {
37007
+ if (getHttpStatusCode(error48) === 401)
37008
+ throw error48;
37009
+ failedRepos++;
37010
+ if (isRateLimitError(error48)) {
37011
+ rateLimitFailures++;
37012
+ }
37013
+ warn(MODULE8, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
37014
+ }
37015
+ }
37016
+ const allReposFailed = failedRepos === repos.length && repos.length > 0;
37017
+ const rateLimitHit = rateLimitFailures > 0;
37018
+ if (allReposFailed) {
37019
+ warn(MODULE8, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
37020
+ }
37021
+ return { candidates, allReposFailed, rateLimitHit };
36902
37022
  }
36903
37023
  async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage) {
36904
37024
  const labelChunks = chunkLabels(labels, reservedOps);
@@ -36954,50 +37074,9 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
36954
37074
  }
36955
37075
  return { candidates: starFiltered, allVetFailed, rateLimitHit };
36956
37076
  }
36957
- async function searchInRepos(octokit, vetter, repos, baseQualifiers, labels, maxResults, priority, filterFn) {
36958
- const candidates = [];
36959
- const batches = batchRepos(repos, BATCH_SIZE);
36960
- let failedBatches = 0;
36961
- let rateLimitFailures = 0;
36962
- for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
36963
- const batch = batches[batchIdx];
36964
- if (candidates.length >= maxResults)
36965
- break;
36966
- if (batchIdx > 0)
36967
- await sleep(INTER_QUERY_DELAY_MS);
36968
- try {
36969
- const repoFilter = batch.map((r) => `repo:${r}`).join(" OR ");
36970
- const repoOps = batch.length - 1;
36971
- const perPage = Math.min(30, (maxResults - candidates.length) * 3);
36972
- const allItems = await searchWithChunkedLabels(octokit, labels, repoOps, (labelQ) => `${baseQualifiers} ${labelQ} (${repoFilter})`.replace(/ +/g, " ").trim(), perPage);
36973
- if (allItems.length > 0) {
36974
- const filtered = filterFn(allItems);
36975
- const remainingNeeded = maxResults - candidates.length;
36976
- const { candidates: vetted, rateLimitHit: vetRateLimitHit } = await vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((i) => i.html_url), remainingNeeded, priority);
36977
- candidates.push(...vetted);
36978
- if (vetRateLimitHit)
36979
- rateLimitFailures++;
36980
- }
36981
- } catch (error48) {
36982
- failedBatches++;
36983
- if (isRateLimitError(error48)) {
36984
- rateLimitFailures++;
36985
- }
36986
- const batchReposStr = batch.join(", ");
36987
- warn(MODULE8, `Error searching issues in batch [${batchReposStr}]:`, errorMessage(error48));
36988
- }
36989
- }
36990
- const allBatchesFailed = failedBatches === batches.length && batches.length > 0;
36991
- const rateLimitHit = rateLimitFailures > 0;
36992
- if (allBatchesFailed) {
36993
- warn(MODULE8, `All ${batches.length} batch(es) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
36994
- }
36995
- return { candidates, allBatchesFailed, rateLimitHit };
36996
- }
36997
37077
 
36998
37078
  // ../core/dist/core/issue-discovery.js
36999
37079
  var MODULE9 = "issue-discovery";
37000
- var INTER_PHASE_DELAY_MS = 2e3;
37001
37080
  var LOW_BUDGET_THRESHOLD = 20;
37002
37081
  var CRITICAL_BUDGET_THRESHOLD = 10;
37003
37082
  function buildIssueFilter(config2) {
@@ -37029,24 +37108,25 @@ function buildIssueFilter(config2) {
37029
37108
  });
37030
37109
  };
37031
37110
  }
37032
- async function runPhase0(octokit, vetter, repos, baseQualifiers, maxResults, filterIssues) {
37111
+ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
37033
37112
  info(MODULE9, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
37034
- const { candidates, allBatchesFailed, rateLimitHit } = await searchInRepos(octokit, vetter, repos, baseQualifiers, [], maxResults, "merged_pr", filterIssues);
37113
+ const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
37035
37114
  info(MODULE9, `Found ${candidates.length} candidates from merged-PR repos`);
37036
37115
  return {
37037
37116
  candidates,
37038
- error: allBatchesFailed ? "All merged-PR repo batches failed" : null,
37117
+ error: allReposFailed ? "All merged-PR repo fetches failed" : null,
37039
37118
  rateLimitHit
37040
37119
  };
37041
37120
  }
37042
- async function runPhase1(octokit, vetter, repos, baseQualifiers, labels, maxResults, filterIssues) {
37121
+ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
37043
37122
  info(MODULE9, `Phase 1: Searching issues in ${repos.length} starred repos...`);
37044
37123
  const phase1Labels = labels.slice(0, 3);
37045
- const { candidates, allBatchesFailed, rateLimitHit } = await searchInRepos(octokit, vetter, repos.slice(0, 10), baseQualifiers, phase1Labels, maxResults, "starred", filterIssues);
37124
+ const reposToSearch = repos.slice(0, 10);
37125
+ const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
37046
37126
  info(MODULE9, `Found ${candidates.length} candidates from starred repos`);
37047
37127
  return {
37048
37128
  candidates,
37049
- error: allBatchesFailed ? "All starred repo batches failed" : null,
37129
+ error: allReposFailed ? "All starred repo fetches failed" : null,
37050
37130
  rateLimitHit
37051
37131
  };
37052
37132
  }
@@ -37112,8 +37192,32 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
37112
37192
  rateLimitHit
37113
37193
  };
37114
37194
  }
37115
- async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37195
+ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
37116
37196
  info(MODULE9, "Phase 3: Searching actively maintained repos...");
37197
+ const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37198
+ const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
37199
+ if (eligibleStarred.length > 0) {
37200
+ info(MODULE9, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
37201
+ const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
37202
+ if (restItems.length > 0) {
37203
+ try {
37204
+ const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
37205
+ if (candidates.length > 0) {
37206
+ info(MODULE9, `Found ${candidates.length} candidates from maintained-repo REST search`);
37207
+ return {
37208
+ candidates,
37209
+ error: allVetFailed ? "all vetting failed" : null,
37210
+ rateLimitHit: vetRateLimitHit
37211
+ };
37212
+ }
37213
+ } catch (error48) {
37214
+ if (getHttpStatusCode(error48) === 401)
37215
+ throw error48;
37216
+ warn(MODULE9, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
37217
+ }
37218
+ }
37219
+ }
37220
+ info(MODULE9, "Phase 3: Falling back to Search API...");
37117
37221
  const thirtyDaysAgo = /* @__PURE__ */ new Date();
37118
37222
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
37119
37223
  const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
@@ -37128,7 +37232,6 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
37128
37232
  per_page: maxResults * 3
37129
37233
  });
37130
37234
  info(MODULE9, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
37131
- const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37132
37235
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
37133
37236
  info(MODULE9, `Found ${candidates.length} candidates from maintained-repo search`);
37134
37237
  return {
@@ -37204,6 +37307,7 @@ var IssueDiscovery = class {
37204
37307
  const labels = options.labels || (scopes ? buildEffectiveLabels(scopes, config2.labels) : config2.labels);
37205
37308
  const maxResults = options.maxResults || 10;
37206
37309
  const minStars = config2.minStars ?? 50;
37310
+ const interPhaseDelay = config2.interPhaseDelayMs ?? 3e4;
37207
37311
  const ALL_STRATEGIES = CONCRETE_STRATEGIES;
37208
37312
  const rawStrategies = options.strategies ?? config2.defaultStrategy ?? ["all"];
37209
37313
  const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
@@ -37239,6 +37343,7 @@ var IssueDiscovery = class {
37239
37343
  return { candidates: [], strategiesUsed: [] };
37240
37344
  }
37241
37345
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
37346
+ const openPRRepos = this.stateReader.getReposWithOpenPRs();
37242
37347
  const starredRepos = this.getStarredRepos();
37243
37348
  const starredRepoSet = new Set(starredRepos);
37244
37349
  const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
@@ -37259,12 +37364,21 @@ var IssueDiscovery = class {
37259
37364
  now: /* @__PURE__ */ new Date(),
37260
37365
  includeDocIssues: config2.includeDocIssues ?? true
37261
37366
  });
37262
- const phase0Repos = mergedPRRepos.slice(0, 10);
37367
+ const seenPhase0 = /* @__PURE__ */ new Set();
37368
+ const phase0Repos = [];
37369
+ for (const repo of [...mergedPRRepos, ...openPRRepos]) {
37370
+ if (seenPhase0.has(repo))
37371
+ continue;
37372
+ seenPhase0.add(repo);
37373
+ phase0Repos.push(repo);
37374
+ if (phase0Repos.length >= 10)
37375
+ break;
37376
+ }
37263
37377
  const phase0RepoSet = new Set(phase0Repos);
37264
37378
  if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
37265
37379
  const remaining = maxResults - allCandidates.length;
37266
37380
  if (remaining > 0) {
37267
- const result = await runPhase0(this.octokit, this.vetter, phase0Repos, baseQualifiers, remaining, filterIssues);
37381
+ const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
37268
37382
  allCandidates.push(...result.candidates);
37269
37383
  phaseErrors["0"] = result.error;
37270
37384
  if (result.rateLimitHit)
@@ -37273,12 +37387,15 @@ var IssueDiscovery = class {
37273
37387
  strategiesUsed.push("merged");
37274
37388
  }
37275
37389
  if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
37276
- await sleep(INTER_PHASE_DELAY_MS);
37390
+ if (interPhaseDelay > 0) {
37391
+ info(MODULE9, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
37392
+ await sleep(interPhaseDelay);
37393
+ }
37277
37394
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
37278
37395
  if (reposToSearch.length > 0) {
37279
37396
  const remaining = maxResults - allCandidates.length;
37280
37397
  if (remaining > 0) {
37281
- const result = await runPhase1(this.octokit, this.vetter, reposToSearch, baseQualifiers, labels, remaining, filterIssues);
37398
+ const result = await runPhase1(this.octokit, this.vetter, reposToSearch, labels, remaining, filterIssues);
37282
37399
  allCandidates.push(...result.candidates);
37283
37400
  phaseErrors["1"] = result.error;
37284
37401
  if (result.rateLimitHit)
@@ -37287,20 +37404,38 @@ var IssueDiscovery = class {
37287
37404
  }
37288
37405
  strategiesUsed.push("starred");
37289
37406
  }
37407
+ const broadDelay = config2.broadPhaseDelayMs ?? 9e4;
37408
+ const skipThreshold = config2.skipBroadWhenSufficientResults ?? 15;
37290
37409
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
37291
- await sleep(INTER_PHASE_DELAY_MS);
37292
- const remaining = maxResults - allCandidates.length;
37293
- const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37294
- allCandidates.push(...result.candidates);
37295
- phaseErrors["2"] = result.error;
37296
- if (result.rateLimitHit)
37297
- rateLimitHitDuringSearch = true;
37410
+ if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
37411
+ info(MODULE9, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
37412
+ } else {
37413
+ if (interPhaseDelay > 0) {
37414
+ info(MODULE9, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
37415
+ await sleep(interPhaseDelay);
37416
+ }
37417
+ if (allCandidates.length > 0 && broadDelay > 0) {
37418
+ info(MODULE9, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
37419
+ await sleep(broadDelay);
37420
+ } else if (allCandidates.length === 0) {
37421
+ info(MODULE9, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
37422
+ }
37423
+ const remaining = maxResults - allCandidates.length;
37424
+ const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37425
+ allCandidates.push(...result.candidates);
37426
+ phaseErrors["2"] = result.error;
37427
+ if (result.rateLimitHit)
37428
+ rateLimitHitDuringSearch = true;
37429
+ }
37298
37430
  strategiesUsed.push("broad");
37299
37431
  }
37300
37432
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
37301
- await sleep(INTER_PHASE_DELAY_MS);
37433
+ if (interPhaseDelay > 0) {
37434
+ info(MODULE9, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
37435
+ await sleep(interPhaseDelay);
37436
+ }
37302
37437
  const remaining = maxResults - allCandidates.length;
37303
- const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37438
+ const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues);
37304
37439
  allCandidates.push(...result.candidates);
37305
37440
  phaseErrors["3"] = result.error;
37306
37441
  if (result.rateLimitHit)
@@ -37602,6 +37737,7 @@ function mergeStates(local, remote) {
37602
37737
  starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
37603
37738
  mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
37604
37739
  closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
37740
+ openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
37605
37741
  savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
37606
37742
  skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
37607
37743
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
@@ -37884,6 +38020,16 @@ var OssScout = class {
37884
38020
  }
37885
38021
  return [...repoCounts.entries()].sort((a, b) => b[1] - a[1]).map(([repo]) => repo);
37886
38022
  }
38023
+ getReposWithOpenPRs() {
38024
+ const repoCounts = /* @__PURE__ */ new Map();
38025
+ for (const pr of this.state.openPRs ?? []) {
38026
+ const repo = extractRepoFromUrl(pr.url);
38027
+ if (repo) {
38028
+ repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
38029
+ }
38030
+ }
38031
+ return [...repoCounts.entries()].sort((a, b) => b[1] - a[1]).map(([repo]) => repo);
38032
+ }
37887
38033
  getStarredRepos() {
37888
38034
  return this.state.starredRepos;
37889
38035
  }
@@ -37932,6 +38078,20 @@ var OssScout = class {
37932
38078
  this.updateRepoScoreFromPRs(pr.repo);
37933
38079
  this.dirty = true;
37934
38080
  }
38081
+ /**
38082
+ * Record that a PR is currently open in this repo.
38083
+ * Open PRs signal active engagement even when nothing is merged yet.
38084
+ */
38085
+ recordOpenPR(pr) {
38086
+ const existing = this.state.openPRs ?? [];
38087
+ if (existing.some((p) => p.url === pr.url))
38088
+ return;
38089
+ this.state.openPRs = [
38090
+ ...existing,
38091
+ { url: pr.url, title: pr.title, openedAt: pr.openedAt }
38092
+ ];
38093
+ this.dirty = true;
38094
+ }
37935
38095
  /**
37936
38096
  * Update repo score with observed signals.
37937
38097
  */
@@ -38313,7 +38473,10 @@ function registerTools(server, scout) {
38313
38473
  const NUMBER_KEYS = /* @__PURE__ */ new Set([
38314
38474
  "minStars",
38315
38475
  "maxIssueAgeDays",
38316
- "minRepoScoreThreshold"
38476
+ "minRepoScoreThreshold",
38477
+ "interPhaseDelayMs",
38478
+ "broadPhaseDelayMs",
38479
+ "skipBroadWhenSufficientResults"
38317
38480
  ]);
38318
38481
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
38319
38482
  server.tool(
package/dist/tools.js CHANGED
@@ -181,6 +181,9 @@ export function registerTools(server, scout) {
181
181
  "minStars",
182
182
  "maxIssueAgeDays",
183
183
  "minRepoScoreThreshold",
184
+ "interPhaseDelayMs",
185
+ "broadPhaseDelayMs",
186
+ "skipBroadWhenSufficientResults",
184
187
  ]);
185
188
  const BOOLEAN_KEYS = new Set(["includeDocIssues"]);
186
189
  server.tool("config-set", "Update an oss-scout configuration preference", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for oss-scout — exposes issue discovery and vetting as MCP tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,15 +18,16 @@
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.27.1",
20
20
  "zod": "^4.3.6",
21
- "@oss-scout/core": "^0.3.0"
21
+ "@oss-scout/core": "^0.5.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.5.0",
25
+ "@vitest/coverage-v8": "^4.1.4",
25
26
  "esbuild": "^0.27.4",
26
27
  "tsx": "^4.21.0",
27
28
  "typescript": "^5.9.3",
28
- "@vitest/coverage-v8": "^4.1.0",
29
- "vitest": "^4.1.0"
29
+ "vite": "^8.0.5",
30
+ "vitest": "^4.1.4"
30
31
  },
31
32
  "keywords": [
32
33
  "mcp",