@oss-scout/mcp 0.9.0 → 0.10.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/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { registerResources } from "./resources.js";
12
12
  /** Server identity, kept in one place for the bin and any embedding host. */
13
13
  export declare const SERVER_INFO: {
14
14
  readonly name: "oss-scout-mcp";
15
- readonly version: "0.9.0";
15
+ readonly version: "0.10.0";
16
16
  };
17
17
  /**
18
18
  * Build a fully-wired MCP server (tools + resources) around a scout.
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export { registerResources } from "./resources.js";
15
15
  /** Server identity, kept in one place for the bin and any embedding host. */
16
16
  export const SERVER_INFO = {
17
17
  name: "oss-scout-mcp",
18
- version: "0.9.0", // x-release-please-version
18
+ version: "0.10.0", // x-release-please-version
19
19
  };
20
20
  /**
21
21
  * Build a fully-wired MCP server (tools + resources) around a scout.
@@ -35867,6 +35867,9 @@ var CONCRETE_STRATEGIES = [
35867
35867
  ];
35868
35868
  var RepoSignalsSchema = external_exports3.looseObject({
35869
35869
  hasActiveMaintainers: external_exports3.boolean(),
35870
+ // Retained for backward compatibility but no longer affects the repo score
35871
+ // (#167): nothing computes it, and hasActiveMaintainers is the live activity
35872
+ // proxy. Kept so old persisted state and the search JSON output still parse.
35870
35873
  isResponsive: external_exports3.boolean(),
35871
35874
  hasHostileComments: external_exports3.boolean()
35872
35875
  });
@@ -36009,6 +36012,12 @@ var ScoutPreferencesSchema = external_exports3.looseObject({
36009
36012
  preferLanguages: external_exports3.array(external_exports3.string()).default([]),
36010
36013
  preferRepos: external_exports3.array(external_exports3.string()).default([]),
36011
36014
  diversityRatio: external_exports3.number().min(0).max(1).default(0),
36015
+ // Soft penalty (milder than the hard excludeRepos filter): candidates in
36016
+ // these `owner/repo` slugs are pushed down the ranking but not removed (#168).
36017
+ avoidRepos: external_exports3.array(external_exports3.string()).default([]),
36018
+ // Soft boost for candidates whose issue labels match one of these types,
36019
+ // case-insensitive (e.g. "bug", "good first issue") (#168).
36020
+ boostIssueTypes: external_exports3.array(external_exports3.string()).default([]),
36012
36021
  broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
36013
36022
  /**
36014
36023
  * Skip the expensive broad phase once this many candidates were found by
@@ -36725,7 +36734,7 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
36725
36734
  }
36726
36735
  }
36727
36736
  var MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1e3;
36728
- async function checkUserMergedPRsInRepo(octokit, owner, repo) {
36737
+ async function checkUserMergedPRsInRepo(octokit, owner, repo, tracker = getSearchBudgetTracker()) {
36729
36738
  const cache = getHttpCache();
36730
36739
  const cacheKey = versionedCacheKey(`merged-prs:${owner}/${repo}`);
36731
36740
  return withInflightDedup(cache, cacheKey, async () => {
@@ -36734,7 +36743,6 @@ async function checkUserMergedPRsInRepo(octokit, owner, repo) {
36734
36743
  return cached2;
36735
36744
  }
36736
36745
  try {
36737
- const tracker = getSearchBudgetTracker();
36738
36746
  await tracker.waitForBudget();
36739
36747
  try {
36740
36748
  const { data } = await octokit.search.issuesAndPullRequests({
@@ -36801,8 +36809,30 @@ function analyzeRequirements(body) {
36801
36809
  return indicators.filter(Boolean).length >= 2;
36802
36810
  }
36803
36811
 
36812
+ // ../core/dist/core/probe-repo-file.js
36813
+ var MODULE6 = "probe-repo-file";
36814
+ async function probeRepoFile(octokit, owner, repo, path5) {
36815
+ try {
36816
+ const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
36817
+ if (data && typeof data === "object" && "content" in data && typeof data.content === "string") {
36818
+ return {
36819
+ text: Buffer.from(data.content, "base64").toString("utf-8"),
36820
+ transient: false
36821
+ };
36822
+ }
36823
+ return { text: null, transient: false };
36824
+ } catch (error48) {
36825
+ const status = getHttpStatusCode(error48);
36826
+ if (status === 404)
36827
+ return { text: null, transient: false };
36828
+ rethrowIfFatal(error48);
36829
+ warn(MODULE6, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
36830
+ return { text: null, transient: true };
36831
+ }
36832
+ }
36833
+
36804
36834
  // ../core/dist/core/repo-health.js
36805
- var MODULE6 = "repo-health";
36835
+ var MODULE7 = "repo-health";
36806
36836
  var guidelinesCache = /* @__PURE__ */ new Map();
36807
36837
  var CACHE_TTL_MS = 60 * 60 * 1e3;
36808
36838
  var HEALTH_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
@@ -36855,7 +36885,7 @@ async function checkProjectHealth(octokit, owner, repo) {
36855
36885
  } catch (error48) {
36856
36886
  rethrowIfFatal(error48);
36857
36887
  const errMsg = errorMessage(error48);
36858
- warn(MODULE6, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
36888
+ warn(MODULE7, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
36859
36889
  return {
36860
36890
  repo: `${owner}/${repo}`,
36861
36891
  checkFailed: true,
@@ -36889,12 +36919,7 @@ async function fetchContributionGuidelinesUncached(octokit, owner, repo) {
36889
36919
  "docs/CONTRIBUTING.md",
36890
36920
  "contributing.md"
36891
36921
  ];
36892
- const results = await Promise.allSettled(filesToCheck.map((file2) => octokit.repos.getContent({ owner, repo, path: file2 }).then(({ data }) => {
36893
- if ("content" in data) {
36894
- return Buffer.from(data.content, "base64").toString("utf-8");
36895
- }
36896
- return null;
36897
- })));
36922
+ const results = await Promise.allSettled(filesToCheck.map((file2) => probeRepoFile(octokit, owner, repo, file2)));
36898
36923
  for (const result of results) {
36899
36924
  if (result.status !== "rejected")
36900
36925
  continue;
@@ -36902,20 +36927,13 @@ async function fetchContributionGuidelinesUncached(octokit, owner, repo) {
36902
36927
  throw result.reason;
36903
36928
  }
36904
36929
  }
36905
- for (let i = 0; i < results.length; i++) {
36906
- const result = results[i];
36907
- if (result.status === "fulfilled" && result.value) {
36908
- const guidelines = parseContributionGuidelines(result.value);
36930
+ for (const result of results) {
36931
+ if (result.status === "fulfilled" && result.value.text) {
36932
+ const guidelines = parseContributionGuidelines(result.value.text);
36909
36933
  guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
36910
36934
  pruneCache();
36911
36935
  return guidelines;
36912
36936
  }
36913
- if (result.status === "rejected") {
36914
- const status = getHttpStatusCode(result.reason);
36915
- if (status !== 404) {
36916
- warn(MODULE6, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${errorMessage(result.reason)}`);
36917
- }
36918
- }
36919
36937
  }
36920
36938
  guidelinesCache.set(cacheKey, {
36921
36939
  guidelines: void 0,
@@ -36964,7 +36982,6 @@ function parseContributionGuidelines(content) {
36964
36982
  }
36965
36983
 
36966
36984
  // ../core/dist/core/anti-llm-policy.js
36967
- var MODULE7 = "anti-llm-policy";
36968
36985
  var POLICY_SCAN_CACHE_TTL_MS = 60 * 60 * 1e3;
36969
36986
  var ANTI_LLM_KEYWORDS = [
36970
36987
  "no ai-generated",
@@ -37027,27 +37044,8 @@ var SOURCE_FILE_FAMILIES = [
37027
37044
  paths: ["README.md", "readme.md", "Readme.md"]
37028
37045
  }
37029
37046
  ];
37030
- async function fetchFileText(octokit, owner, repo, path5) {
37031
- try {
37032
- const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
37033
- if ("content" in data && typeof data.content === "string") {
37034
- return {
37035
- text: Buffer.from(data.content, "base64").toString("utf-8"),
37036
- transient: false
37037
- };
37038
- }
37039
- return { text: null, transient: false };
37040
- } catch (error48) {
37041
- const status = getHttpStatusCode(error48);
37042
- if (status === 404)
37043
- return { text: null, transient: false };
37044
- rethrowIfFatal(error48);
37045
- warn(MODULE7, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
37046
- return { text: null, transient: true };
37047
- }
37048
- }
37049
37047
  async function fetchFamilyText(octokit, owner, repo, paths) {
37050
- const results = await Promise.allSettled(paths.map((p) => fetchFileText(octokit, owner, repo, p)));
37048
+ const results = await Promise.allSettled(paths.map((p) => probeRepoFile(octokit, owner, repo, p)));
37051
37049
  let hadTransientFailure = false;
37052
37050
  for (const result of results) {
37053
37051
  if (result.status === "fulfilled") {
@@ -37377,9 +37375,18 @@ function deriveRecommendation(input) {
37377
37375
  var IssueVetter = class {
37378
37376
  octokit;
37379
37377
  stateReader;
37380
- constructor(octokit, stateReader) {
37378
+ budgetTracker;
37379
+ /**
37380
+ * @param octokit - Authenticated Octokit instance
37381
+ * @param stateReader - Read-only scout state interface
37382
+ * @param budgetTracker - Search budget tracker. Defaults to the shared
37383
+ * singleton so existing callers behave identically; inject a per-search
37384
+ * instance to isolate budget accounting in a long-lived concurrent host.
37385
+ */
37386
+ constructor(octokit, stateReader, budgetTracker = getSearchBudgetTracker()) {
37381
37387
  this.octokit = octokit;
37382
37388
  this.stateReader = stateReader;
37389
+ this.budgetTracker = budgetTracker;
37383
37390
  }
37384
37391
  /**
37385
37392
  * Vet a specific issue — runs all checks and computes recommendation + viability score.
@@ -37413,7 +37420,7 @@ var IssueVetter = class {
37413
37420
  checkNotClaimed(this.octokit, owner, repo, number4, core.commentCount),
37414
37421
  checkProjectHealth(this.octokit, owner, repo),
37415
37422
  fetchContributionGuidelines(this.octokit, owner, repo),
37416
- hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo)
37423
+ hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo, this.budgetTracker)
37417
37424
  ]);
37418
37425
  const antiLLMPolicy = await fetchAndScanAntiLLMPolicy(this.octokit, owner, repo, { contributingText: contributionGuidelines?.rawContent });
37419
37426
  const noExistingPR = existingPRCheck.passed;
@@ -37676,7 +37683,7 @@ function interleaveArrays(arrays) {
37676
37683
  return result;
37677
37684
  }
37678
37685
  var SEARCH_CACHE_TTL_MS = 15 * 60 * 1e3;
37679
- async function cachedSearchIssues(octokit, params) {
37686
+ async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTracker()) {
37680
37687
  const cacheKey = versionedCacheKey(`search:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
37681
37688
  const cache = getHttpCache();
37682
37689
  const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
@@ -37684,7 +37691,6 @@ async function cachedSearchIssues(octokit, params) {
37684
37691
  debug(MODULE10, `Search cache hit for query`);
37685
37692
  return cached2;
37686
37693
  }
37687
- const tracker = getSearchBudgetTracker();
37688
37694
  await tracker.waitForBudget();
37689
37695
  let data;
37690
37696
  try {
@@ -37819,7 +37825,7 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
37819
37825
  }
37820
37826
  return { candidates, allReposFailed, rateLimitHit };
37821
37827
  }
37822
- async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage) {
37828
+ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage, tracker = getSearchBudgetTracker()) {
37823
37829
  const labelChunks = chunkLabels(labels, reservedOps);
37824
37830
  const seenUrls = /* @__PURE__ */ new Set();
37825
37831
  const allItems = [];
@@ -37832,7 +37838,7 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
37832
37838
  sort: "created",
37833
37839
  order: "desc",
37834
37840
  per_page: perPage
37835
- });
37841
+ }, tracker);
37836
37842
  for (const item of data.items) {
37837
37843
  if (!seenUrls.has(item.html_url)) {
37838
37844
  seenUrls.add(item.html_url);
@@ -37851,14 +37857,14 @@ function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
37851
37857
  return [languages.map((l) => `language:${l}`).join(" ")];
37852
37858
  return languages.map((l) => `language:${l}`);
37853
37859
  }
37854
- async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage) {
37860
+ async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker()) {
37855
37861
  const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
37856
37862
  const seenUrls = /* @__PURE__ */ new Set();
37857
37863
  const allItems = [];
37858
37864
  for (let i = 0; i < langVariants.length; i++) {
37859
37865
  if (i > 0)
37860
37866
  await sleep(INTER_QUERY_DELAY_MS);
37861
- const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage);
37867
+ const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage, tracker);
37862
37868
  for (const item of items) {
37863
37869
  if (!seenUrls.has(item.html_url)) {
37864
37870
  seenUrls.add(item.html_url);
@@ -37903,18 +37909,27 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37903
37909
  // ../core/dist/core/personalization.js
37904
37910
  var REPO_BOOST = 20;
37905
37911
  var LANGUAGE_BOOST = 10;
37912
+ var ISSUE_TYPE_BOOST = 10;
37913
+ var AVOID_PENALTY = 15;
37906
37914
  function boostScoreOf(candidate) {
37907
37915
  return candidate.personalization?.kind === "boosted" ? candidate.personalization.score : 0;
37908
37916
  }
37909
- function annotateBoost(candidates, preferLanguages, preferRepos) {
37910
- const langSet = new Set((preferLanguages ?? []).map((l) => l.trim().toLowerCase()).filter(Boolean));
37911
- const repoSet = new Set((preferRepos ?? []).map((r) => r.trim().toLowerCase()).filter(Boolean));
37912
- if (langSet.size === 0 && repoSet.size === 0)
37917
+ function normalizeSet(values) {
37918
+ return new Set((values ?? []).map((v) => v.trim().toLowerCase()).filter(Boolean));
37919
+ }
37920
+ function annotateBoost(candidates, bias = {}) {
37921
+ const langSet = normalizeSet(bias.preferLanguages);
37922
+ const repoSet = normalizeSet(bias.preferRepos);
37923
+ const avoidSet = normalizeSet(bias.avoidRepos);
37924
+ const typeSet = normalizeSet(bias.boostIssueTypes);
37925
+ if (langSet.size === 0 && repoSet.size === 0 && avoidSet.size === 0 && typeSet.size === 0) {
37913
37926
  return candidates;
37927
+ }
37914
37928
  return candidates.map((c) => {
37915
37929
  let score = 0;
37916
37930
  const reasons = [];
37917
- if (repoSet.size > 0 && repoSet.has(c.issue.repo.toLowerCase())) {
37931
+ const repoLower = c.issue.repo.toLowerCase();
37932
+ if (repoSet.size > 0 && repoSet.has(repoLower)) {
37918
37933
  score += REPO_BOOST;
37919
37934
  reasons.push(`repo affinity: ${c.issue.repo}`);
37920
37935
  }
@@ -37923,7 +37938,18 @@ function annotateBoost(candidates, preferLanguages, preferRepos) {
37923
37938
  score += LANGUAGE_BOOST;
37924
37939
  reasons.push(`language match: ${lang}`);
37925
37940
  }
37926
- if (score === 0)
37941
+ if (typeSet.size > 0) {
37942
+ const matched = c.issue.labels.find((l) => typeSet.has(l.toLowerCase()));
37943
+ if (matched) {
37944
+ score += ISSUE_TYPE_BOOST;
37945
+ reasons.push(`issue type: ${matched}`);
37946
+ }
37947
+ }
37948
+ if (avoidSet.size > 0 && avoidSet.has(repoLower)) {
37949
+ score -= AVOID_PENALTY;
37950
+ reasons.push(`avoided repo: ${c.issue.repo}`);
37951
+ }
37952
+ if (reasons.length === 0)
37927
37953
  return c;
37928
37954
  return { ...c, personalization: { kind: "boosted", score, reasons } };
37929
37955
  });
@@ -37951,7 +37977,7 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
37951
37977
  break;
37952
37978
  if (seen.has(c.issue.url))
37953
37979
  continue;
37954
- if (boostScoreOf(c) > 0)
37980
+ if (boostScoreOf(c) !== 0)
37955
37981
  continue;
37956
37982
  picks.push({ ...c, personalization: { kind: "diversity" } });
37957
37983
  seen.add(c.issue.url);
@@ -38023,7 +38049,7 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
38023
38049
  rateLimitHit
38024
38050
  };
38025
38051
  }
38026
- async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
38052
+ async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues, tracker) {
38027
38053
  info(MODULE11, "Phase 2: General issue search...");
38028
38054
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38029
38055
  const tierLabelGroups = [];
@@ -38050,7 +38076,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38050
38076
  let rateLimitHit = false;
38051
38077
  for (const { tier, tierLabels } of tierLabelGroups) {
38052
38078
  try {
38053
- const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
38079
+ const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3, tracker);
38054
38080
  info(MODULE11, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
38055
38081
  const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
38056
38082
  tierResults.push(tierCandidates);
@@ -38085,7 +38111,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38085
38111
  rateLimitHit
38086
38112
  };
38087
38113
  }
38088
- async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
38114
+ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues, tracker) {
38089
38115
  info(MODULE11, "Phase 3: Searching actively maintained repos...");
38090
38116
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38091
38117
  const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
@@ -38123,7 +38149,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38123
38149
  sort: "updated",
38124
38150
  order: "desc",
38125
38151
  per_page: maxResults * 3
38126
- });
38152
+ }, tracker);
38127
38153
  info(MODULE11, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
38128
38154
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
38129
38155
  info(MODULE11, `Found ${candidates.length} candidates from maintained-repo search`);
@@ -38150,19 +38176,26 @@ var IssueDiscovery = class {
38150
38176
  octokit;
38151
38177
  githubToken;
38152
38178
  vetter;
38179
+ budgetTracker;
38153
38180
  /** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
38154
38181
  rateLimitWarning = null;
38155
38182
  /**
38156
38183
  * @param githubToken - GitHub personal access token or token from `gh auth token`
38157
38184
  * @param preferences - User's search preferences (languages, labels, scopes, etc.)
38158
38185
  * @param stateReader - Read-only interface for accessing scout state (merged PRs, starred repos, etc.)
38186
+ * @param budgetTracker - Search budget tracker. Defaults to the shared
38187
+ * singleton so existing callers behave identically. A long-lived host
38188
+ * serving concurrent searches can inject a per-search instance so one
38189
+ * search's init() no longer resets the budget state of another (the
38190
+ * shared-singleton concurrency hazard, #156).
38159
38191
  */
38160
- constructor(githubToken, preferences, stateReader) {
38192
+ constructor(githubToken, preferences, stateReader, budgetTracker = getSearchBudgetTracker()) {
38161
38193
  this.preferences = preferences;
38162
38194
  this.stateReader = stateReader;
38163
38195
  this.githubToken = githubToken;
38164
38196
  this.octokit = getOctokit(githubToken);
38165
- this.vetter = new IssueVetter(this.octokit, this.stateReader);
38197
+ this.budgetTracker = budgetTracker;
38198
+ this.vetter = new IssueVetter(this.octokit, this.stateReader, this.budgetTracker);
38166
38199
  }
38167
38200
  /**
38168
38201
  * Get starred repos from the state reader.
@@ -38211,8 +38244,20 @@ var IssueDiscovery = class {
38211
38244
  const allCandidates = [];
38212
38245
  const phaseErrors = {};
38213
38246
  let rateLimitHitDuringSearch = false;
38247
+ const applyInterPhaseDelay = async () => {
38248
+ if (interPhaseDelay > 0) {
38249
+ info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38250
+ await sleep(interPhaseDelay);
38251
+ }
38252
+ };
38253
+ const recordPhaseResult = (key, result) => {
38254
+ allCandidates.push(...result.candidates);
38255
+ phaseErrors[key] = result.error;
38256
+ if (result.rateLimitHit)
38257
+ rateLimitHitDuringSearch = true;
38258
+ };
38214
38259
  this.rateLimitWarning = null;
38215
- const tracker = getSearchBudgetTracker();
38260
+ const tracker = this.budgetTracker;
38216
38261
  let searchBudget = LOW_BUDGET_THRESHOLD - 1;
38217
38262
  try {
38218
38263
  const rateLimit = await checkRateLimit(this.githubToken);
@@ -38274,27 +38319,18 @@ var IssueDiscovery = class {
38274
38319
  const remaining = maxResults - allCandidates.length;
38275
38320
  if (remaining > 0) {
38276
38321
  const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
38277
- allCandidates.push(...result.candidates);
38278
- phaseErrors["0"] = result.error;
38279
- if (result.rateLimitHit)
38280
- rateLimitHitDuringSearch = true;
38322
+ recordPhaseResult("0", result);
38281
38323
  }
38282
38324
  strategiesUsed.push("merged");
38283
38325
  }
38284
38326
  if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
38285
- if (interPhaseDelay > 0) {
38286
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38287
- await sleep(interPhaseDelay);
38288
- }
38327
+ await applyInterPhaseDelay();
38289
38328
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
38290
38329
  if (reposToSearch.length > 0) {
38291
38330
  const remaining = maxResults - allCandidates.length;
38292
38331
  if (remaining > 0) {
38293
38332
  const result = await runPhase1(this.octokit, this.vetter, reposToSearch, labels, remaining, filterIssues);
38294
- allCandidates.push(...result.candidates);
38295
- phaseErrors["1"] = result.error;
38296
- if (result.rateLimitHit)
38297
- rateLimitHitDuringSearch = true;
38333
+ recordPhaseResult("1", result);
38298
38334
  strategiesUsed.push("starred");
38299
38335
  }
38300
38336
  }
@@ -38306,10 +38342,7 @@ var IssueDiscovery = class {
38306
38342
  if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
38307
38343
  info(MODULE11, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
38308
38344
  } else {
38309
- if (interPhaseDelay > 0) {
38310
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38311
- await sleep(interPhaseDelay);
38312
- }
38345
+ await applyInterPhaseDelay();
38313
38346
  if (allCandidates.length > 0 && broadDelay > 0) {
38314
38347
  info(MODULE11, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
38315
38348
  await sleep(broadDelay);
@@ -38317,25 +38350,16 @@ var IssueDiscovery = class {
38317
38350
  info(MODULE11, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
38318
38351
  }
38319
38352
  const remaining = maxResults - allCandidates.length;
38320
- const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
38321
- allCandidates.push(...result.candidates);
38322
- phaseErrors["2"] = result.error;
38323
- if (result.rateLimitHit)
38324
- rateLimitHitDuringSearch = true;
38353
+ const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues, tracker);
38354
+ recordPhaseResult("2", result);
38325
38355
  strategiesUsed.push("broad");
38326
38356
  }
38327
38357
  }
38328
38358
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
38329
- if (interPhaseDelay > 0) {
38330
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38331
- await sleep(interPhaseDelay);
38332
- }
38359
+ await applyInterPhaseDelay();
38333
38360
  const remaining = maxResults - allCandidates.length;
38334
- const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues);
38335
- allCandidates.push(...result.candidates);
38336
- phaseErrors["3"] = result.error;
38337
- if (result.rateLimitHit)
38338
- rateLimitHitDuringSearch = true;
38361
+ const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues, tracker);
38362
+ recordPhaseResult("3", result);
38339
38363
  strategiesUsed.push("maintained");
38340
38364
  }
38341
38365
  const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
@@ -38362,7 +38386,12 @@ var IssueDiscovery = class {
38362
38386
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
38363
38387
  this.rateLimitWarning = `Search results may be incomplete: GitHub API rate limits were hit during search.${budgetNote} Found ${allCandidates.length} candidate${allCandidates.length === 1 ? "" : "s"} but some search phases were limited. Try again after the rate limit resets for complete results.`;
38364
38388
  }
38365
- const ranked = annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
38389
+ const ranked = annotateBoost(allCandidates, {
38390
+ preferLanguages: options.preferLanguages,
38391
+ preferRepos: options.preferRepos,
38392
+ avoidRepos: options.avoidRepos,
38393
+ boostIssueTypes: options.boostIssueTypes
38394
+ });
38366
38395
  ranked.sort((a, b) => {
38367
38396
  const priorityOrder = {
38368
38397
  merged_pr: 0,
@@ -38416,7 +38445,6 @@ var IssueDiscovery = class {
38416
38445
  };
38417
38446
 
38418
38447
  // ../core/dist/core/roadmap.js
38419
- var MODULE12 = "roadmap";
38420
38448
  var CACHE_TTL_MS2 = 60 * 60 * 1e3;
38421
38449
  var ROADMAP_PATHS = [
38422
38450
  "ROADMAP.md",
@@ -38484,22 +38512,13 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
38484
38512
  var roadmapInflight = /* @__PURE__ */ new Map();
38485
38513
  async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
38486
38514
  for (const path5 of ROADMAP_PATHS) {
38487
- try {
38488
- const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
38489
- if (!("content" in data))
38490
- continue;
38491
- const content = Buffer.from(data.content, "base64").toString("utf-8");
38492
- const refs = parseRoadmapIssueRefs(content, owner, repo);
38493
- roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
38494
- pruneCache2();
38495
- return refs;
38496
- } catch (err) {
38497
- rethrowIfFatal(err);
38498
- const status = getHttpStatusCode(err);
38499
- if (status === 404)
38500
- continue;
38501
- warn(MODULE12, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(err)}`);
38502
- }
38515
+ const { text } = await probeRepoFile(octokit, owner, repo, path5);
38516
+ if (!text)
38517
+ continue;
38518
+ const refs = parseRoadmapIssueRefs(text, owner, repo);
38519
+ roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
38520
+ pruneCache2();
38521
+ return refs;
38503
38522
  }
38504
38523
  const empty = /* @__PURE__ */ new Set();
38505
38524
  roadmapCache.set(cacheKey, { refs: empty, fetchedAt: Date.now() });
@@ -38508,7 +38527,7 @@ async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
38508
38527
  }
38509
38528
 
38510
38529
  // ../core/dist/core/feature-discovery.js
38511
- var MODULE13 = "feature-discovery";
38530
+ var MODULE12 = "feature-discovery";
38512
38531
  var INTER_REPO_DELAY_MS = 2e3;
38513
38532
  var MIN_VIABILITY_SCORE = 40;
38514
38533
  var ANCHOR_THRESHOLD = 3;
@@ -38621,7 +38640,7 @@ async function vetAndClassify(item, vetter, roadmapRefs) {
38621
38640
  });
38622
38641
  } catch (err) {
38623
38642
  rethrowIfFatal(err);
38624
- warn(MODULE13, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
38643
+ warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
38625
38644
  return null;
38626
38645
  }
38627
38646
  const horizon = classifyHorizon({
@@ -38664,7 +38683,7 @@ async function discoverFeatures(opts) {
38664
38683
  roadmapRefs = refs;
38665
38684
  } catch (err) {
38666
38685
  rethrowIfFatal(err);
38667
- warn(MODULE13, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
38686
+ warn(MODULE12, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
38668
38687
  continue;
38669
38688
  }
38670
38689
  const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
@@ -38742,7 +38761,7 @@ async function discoverFeaturesBroad(opts) {
38742
38761
  items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).slice(0, maxToVet);
38743
38762
  } catch (err) {
38744
38763
  rethrowIfFatal(err);
38745
- warn(MODULE13, `broad feature search failed: ${errorMessage(err)}`);
38764
+ warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
38746
38765
  return {
38747
38766
  quickWins: [],
38748
38767
  biggerBets: [],
@@ -38769,7 +38788,7 @@ async function discoverFeaturesBroad(opts) {
38769
38788
  // ../core/dist/core/gist-state-store.js
38770
38789
  var fs3 = __toESM(require("fs"), 1);
38771
38790
  var path3 = __toESM(require("path"), 1);
38772
- var MODULE14 = "gist-state";
38791
+ var MODULE13 = "gist-state";
38773
38792
  var GIST_DESCRIPTION = "oss-scout-state";
38774
38793
  var GIST_FILENAME = "state.json";
38775
38794
  var GIST_ID_FILE = "gist-id";
@@ -38814,7 +38833,7 @@ var GistStateStore = class {
38814
38833
  } catch (err) {
38815
38834
  if (getHttpStatusCode(err) === 401)
38816
38835
  throw err;
38817
- warn(MODULE14, `API bootstrap failed: ${errorMessage(err)}`);
38836
+ warn(MODULE13, `API bootstrap failed: ${errorMessage(err)}`);
38818
38837
  return this.bootstrapFromCache(classifyDegradedReason(err));
38819
38838
  }
38820
38839
  }
@@ -38823,7 +38842,7 @@ var GistStateStore = class {
38823
38842
  */
38824
38843
  async push(state) {
38825
38844
  if (!this.gistId) {
38826
- warn(MODULE14, "No gist ID \u2014 cannot push");
38845
+ warn(MODULE13, "No gist ID \u2014 cannot push");
38827
38846
  this.writeCache(state);
38828
38847
  return false;
38829
38848
  }
@@ -38835,12 +38854,12 @@ var GistStateStore = class {
38835
38854
  }
38836
38855
  } catch (err) {
38837
38856
  rethrowIfFatal(err);
38838
- warn(MODULE14, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
38857
+ warn(MODULE13, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
38839
38858
  }
38840
38859
  this.writeCache(toWrite);
38841
38860
  const json2 = JSON.stringify(toWrite, null, 2);
38842
38861
  if (json2.length > 9e5) {
38843
- warn(MODULE14, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
38862
+ warn(MODULE13, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
38844
38863
  return false;
38845
38864
  }
38846
38865
  try {
@@ -38850,11 +38869,11 @@ var GistStateStore = class {
38850
38869
  [GIST_FILENAME]: { content: json2 }
38851
38870
  }
38852
38871
  });
38853
- debug(MODULE14, "State pushed to gist");
38872
+ debug(MODULE13, "State pushed to gist");
38854
38873
  return true;
38855
38874
  } catch (err) {
38856
38875
  rethrowIfFatal(err);
38857
- warn(MODULE14, `Failed to push: ${errorMessage(err)}`);
38876
+ warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
38858
38877
  return false;
38859
38878
  }
38860
38879
  }
@@ -38866,7 +38885,7 @@ var GistStateStore = class {
38866
38885
  async bootstrapFromApi() {
38867
38886
  const cachedId = this.readCachedGistId();
38868
38887
  if (cachedId) {
38869
- debug(MODULE14, `Trying cached gist ID: ${cachedId}`);
38888
+ debug(MODULE13, `Trying cached gist ID: ${cachedId}`);
38870
38889
  try {
38871
38890
  const fetched = await this.fetchGistState(cachedId);
38872
38891
  if (fetched) {
@@ -38878,13 +38897,13 @@ var GistStateStore = class {
38878
38897
  } catch (err) {
38879
38898
  if (getHttpStatusCode(err) !== 404)
38880
38899
  throw err;
38881
- debug(MODULE14, `Cached gist gone (404): ${errorMessage(err)}`);
38900
+ debug(MODULE13, `Cached gist gone (404): ${errorMessage(err)}`);
38882
38901
  }
38883
- debug(MODULE14, "Cached gist ID invalid, searching...");
38902
+ debug(MODULE13, "Cached gist ID invalid, searching...");
38884
38903
  }
38885
38904
  const search = await this.searchForGist();
38886
38905
  if (search.id) {
38887
- debug(MODULE14, `Found gist via search: ${search.id}`);
38906
+ debug(MODULE13, `Found gist via search: ${search.id}`);
38888
38907
  this.saveGistId(search.id);
38889
38908
  this.gistId = search.id;
38890
38909
  const fetched = await this.fetchGistState(search.id);
@@ -38893,13 +38912,13 @@ var GistStateStore = class {
38893
38912
  this.writeCache(state);
38894
38913
  return { gistId: search.id, state, created: false };
38895
38914
  }
38896
- warn(MODULE14, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
38915
+ warn(MODULE13, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
38897
38916
  return this.bootstrapFromCache("unknown");
38898
38917
  }
38899
38918
  if (!search.exhaustive) {
38900
- warn(MODULE14, `Scanned the first ${SEARCH_MAX_PAGES * 100} gists without finding an oss-scout state gist, but the account has more. Creating a new state gist; if one already exists, copy its id into the gist-id file in the oss-scout data directory to avoid a duplicate.`);
38919
+ warn(MODULE13, `Scanned the first ${SEARCH_MAX_PAGES * 100} gists without finding an oss-scout state gist, but the account has more. Creating a new state gist; if one already exists, copy its id into the gist-id file in the oss-scout data directory to avoid a duplicate.`);
38901
38920
  }
38902
- debug(MODULE14, "No existing gist found, creating new one");
38921
+ debug(MODULE13, "No existing gist found, creating new one");
38903
38922
  const freshState = ScoutStateSchema.parse({ version: 1 });
38904
38923
  const newId = await this.createGist(freshState);
38905
38924
  this.saveGistId(newId);
@@ -38910,7 +38929,7 @@ var GistStateStore = class {
38910
38929
  bootstrapFromCache(reason) {
38911
38930
  const cached2 = this.readCache();
38912
38931
  if (cached2) {
38913
- debug(MODULE14, "Bootstrapped from local cache (degraded mode)");
38932
+ debug(MODULE13, "Bootstrapped from local cache (degraded mode)");
38914
38933
  const cachedId = this.readCachedGistId();
38915
38934
  if (cachedId)
38916
38935
  this.gistId = cachedId;
@@ -38922,7 +38941,7 @@ var GistStateStore = class {
38922
38941
  degradedReason: reason
38923
38942
  };
38924
38943
  }
38925
- debug(MODULE14, "No cache available, using fresh state (degraded mode)");
38944
+ debug(MODULE13, "No cache available, using fresh state (degraded mode)");
38926
38945
  const fresh = ScoutStateSchema.parse({ version: 1 });
38927
38946
  return {
38928
38947
  gistId: "",
@@ -38942,7 +38961,7 @@ var GistStateStore = class {
38942
38961
  const parsed = JSON.parse(file2.content);
38943
38962
  return parseScoutState(parsed);
38944
38963
  } catch (err) {
38945
- warn(MODULE14, `Gist content failed validation: ${errorMessage(err)}`);
38964
+ warn(MODULE13, `Gist content failed validation: ${errorMessage(err)}`);
38946
38965
  return null;
38947
38966
  }
38948
38967
  }
@@ -38985,7 +39004,7 @@ var GistStateStore = class {
38985
39004
  } catch (err) {
38986
39005
  const code = err?.code;
38987
39006
  if (code !== "ENOENT") {
38988
- warn(MODULE14, `Failed to read cached gist ID: ${errorMessage(err)}`);
39007
+ warn(MODULE13, `Failed to read cached gist ID: ${errorMessage(err)}`);
38989
39008
  }
38990
39009
  return null;
38991
39010
  }
@@ -39011,7 +39030,7 @@ var GistStateStore = class {
39011
39030
  } catch (err) {
39012
39031
  const code = err?.code;
39013
39032
  if (code !== "ENOENT") {
39014
- warn(MODULE14, `Failed to read state cache: ${errorMessage(err)}`);
39033
+ warn(MODULE13, `Failed to read state cache: ${errorMessage(err)}`);
39015
39034
  }
39016
39035
  return null;
39017
39036
  }
@@ -39022,7 +39041,7 @@ var GistStateStore = class {
39022
39041
  mode: 384
39023
39042
  });
39024
39043
  } catch (err) {
39025
- warn(MODULE14, `Failed to write cache: ${errorMessage(err)}`);
39044
+ warn(MODULE13, `Failed to write cache: ${errorMessage(err)}`);
39026
39045
  }
39027
39046
  }
39028
39047
  };
@@ -39150,7 +39169,7 @@ function pickFresherTimestamp(a, b) {
39150
39169
  // ../core/dist/core/local-state.js
39151
39170
  var fs4 = __toESM(require("fs"), 1);
39152
39171
  var path4 = __toESM(require("path"), 1);
39153
- var MODULE15 = "local-state";
39172
+ var MODULE14 = "local-state";
39154
39173
  function getStatePath() {
39155
39174
  return path4.join(getDataDir(), "state.json");
39156
39175
  }
@@ -39164,13 +39183,13 @@ function loadLocalState() {
39164
39183
  if (code === "ENOENT") {
39165
39184
  return ScoutStateSchema.parse({ version: 1 });
39166
39185
  }
39167
- warn(MODULE15, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
39186
+ warn(MODULE14, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
39168
39187
  try {
39169
39188
  const backupPath = `${statePath}.corrupt.${Date.now()}`;
39170
39189
  fs4.copyFileSync(statePath, backupPath);
39171
- warn(MODULE15, `Corrupt state backed up to ${backupPath}`);
39190
+ warn(MODULE14, `Corrupt state backed up to ${backupPath}`);
39172
39191
  } catch (backupErr) {
39173
- warn(MODULE15, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
39192
+ warn(MODULE14, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
39174
39193
  }
39175
39194
  return ScoutStateSchema.parse({ version: 1 });
39176
39195
  }
@@ -39190,7 +39209,7 @@ function saveLocalState(state) {
39190
39209
  }
39191
39210
  throw err;
39192
39211
  }
39193
- debug(MODULE15, "State saved");
39212
+ debug(MODULE14, "State saved");
39194
39213
  }
39195
39214
 
39196
39215
  // ../core/dist/scout.js
@@ -39309,6 +39328,10 @@ var OssScout = class {
39309
39328
  const prefRepos = prefs.preferRepos ?? [];
39310
39329
  const preferLanguages = options?.preferLanguages ?? (prefLangs.length > 0 ? prefLangs : void 0);
39311
39330
  const preferRepos = options?.preferRepos ?? (prefRepos.length > 0 ? prefRepos : void 0);
39331
+ const prefAvoid = prefs.avoidRepos ?? [];
39332
+ const prefBoostTypes = prefs.boostIssueTypes ?? [];
39333
+ const avoidRepos = options?.avoidRepos ?? (prefAvoid.length > 0 ? prefAvoid : void 0);
39334
+ const boostIssueTypes = options?.boostIssueTypes ?? (prefBoostTypes.length > 0 ? prefBoostTypes : void 0);
39312
39335
  const diversityRatio = options?.diversityRatio ?? prefs.diversityRatio ?? 0;
39313
39336
  const { candidates, strategiesUsed } = await discovery.searchIssues({
39314
39337
  maxResults: options?.maxResults,
@@ -39316,6 +39339,8 @@ var OssScout = class {
39316
39339
  skippedUrls,
39317
39340
  preferLanguages,
39318
39341
  preferRepos,
39342
+ avoidRepos,
39343
+ boostIssueTypes,
39319
39344
  diversityRatio,
39320
39345
  interPhaseDelayMs: options?.interPhaseDelayMs,
39321
39346
  broadPhaseDelayMs: options?.broadPhaseDelayMs
@@ -39339,14 +39364,13 @@ var OssScout = class {
39339
39364
  * calculateScore's +1 active-maintainers weight was inert; `isActive`
39340
39365
  * (recent commit activity) is a real, already-computed proxy.
39341
39366
  *
39342
- * `isResponsive` and `avgResponseDays` are deliberately NOT set here:
39343
- * `projectHealth.avgIssueResponseDays` is a hardcoded `0` placeholder
39344
- * (repo-health.ts), so deriving responsiveness from it would award +1 to
39345
- * every repo — a fake signal worse than the inert one. Real responsiveness
39346
- * needs an actual response-time measurement (extra API calls), deferred.
39347
- * `hasHostileComments` likewise stays a host-settable capability (it needs
39348
- * comment sentiment, out of scope). A failed health check is skipped so its
39349
- * neutral-default fields don't pollute the score.
39367
+ * `isResponsive` and `avgResponseDays` are deliberately NOT set here, and
39368
+ * `isResponsive` no longer carries a score weight at all (#167): real
39369
+ * responsiveness needs an actual response-time measurement (extra API calls)
39370
+ * that is out of scope, and `hasActiveMaintainers` already covers the
39371
+ * activity proxy. `hasHostileComments` stays a host-settable capability (it
39372
+ * needs comment sentiment, out of scope). A failed health check is skipped so
39373
+ * its neutral-default fields don't pollute the score.
39350
39374
  */
39351
39375
  updateRepoSignalsFromHealth(health) {
39352
39376
  if (health.checkFailed)
@@ -39903,16 +39927,21 @@ var OssScout = class {
39903
39927
  });
39904
39928
  }
39905
39929
  /**
39906
- * Calculate repo score (1-10) from observed data.
39930
+ * Calculate repo score from observed data.
39907
39931
  * base 5, +1 per merged PR (max +3), -1 per closed-without-merge (max -3),
39908
- * +1 responsive, +1 active maintainers, -2 hostile comments, clamped 1-10
39932
+ * +1 active maintainers, -2 hostile comments, clamped to [1, 10].
39933
+ *
39934
+ * `isResponsive` is intentionally NOT scored (#167): nothing in oss-scout
39935
+ * ever computes it, so awarding +1 for it was dead weight that the
39936
+ * now-computed `hasActiveMaintainers` signal already covers as the activity
39937
+ * proxy. The field is retained on RepoSignals for backward compatibility but
39938
+ * no longer affects the score. With the current signals the reachable range
39939
+ * is [1, 9]; the upper clamp stays as defensive hygiene.
39909
39940
  */
39910
39941
  calculateScore(repoScore) {
39911
39942
  let score = 5;
39912
39943
  score += Math.min(repoScore.mergedPRCount, 3);
39913
39944
  score -= Math.min(repoScore.closedWithoutMergeCount, 3);
39914
- if (repoScore.signals.isResponsive)
39915
- score += 1;
39916
39945
  if (repoScore.signals.hasActiveMaintainers)
39917
39946
  score += 1;
39918
39947
  if (repoScore.signals.hasHostileComments)
@@ -39949,6 +39978,8 @@ var FIELD_CONFIGS = {
39949
39978
  preferLanguages: { type: "array" },
39950
39979
  preferRepos: { type: "array" },
39951
39980
  diversityRatio: { type: "float" },
39981
+ avoidRepos: { type: "array" },
39982
+ boostIssueTypes: { type: "array" },
39952
39983
  slmTriageModel: { type: "string" },
39953
39984
  slmTriageHost: { type: "string" },
39954
39985
  featuresAnchorThreshold: { type: "number" },
@@ -40067,6 +40098,10 @@ function validateUrl(url2) {
40067
40098
 
40068
40099
  // src/tools.ts
40069
40100
  var TOOL_TIMEOUT_MS = 3e5;
40101
+ var PERSIST_WARNING = "Checkpoint failed: changes were applied in memory but could not be persisted. They may be lost when the server restarts.";
40102
+ function withPersistWarning(payload, persisted) {
40103
+ return persisted ? payload : { ...payload, persistWarning: PERSIST_WARNING };
40104
+ }
40070
40105
  function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
40071
40106
  let timeoutId;
40072
40107
  const timer = new Promise((_, reject) => {
@@ -40092,6 +40127,12 @@ function registerTools(server, scout) {
40092
40127
  preferRepos: external_exports3.array(external_exports3.string()).optional().describe(
40093
40128
  "Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."
40094
40129
  ),
40130
+ avoidRepos: external_exports3.array(external_exports3.string()).optional().describe(
40131
+ "Soft PENALTY (milder than excludeRepos) for candidates in one of these `owner/repo` slugs (#168). Pushes them down the ranking but does not remove them; a strong boost can still outweigh it."
40132
+ ),
40133
+ boostIssueTypes: external_exports3.array(external_exports3.string()).optional().describe(
40134
+ 'Soft-boost ranking for candidates whose issue labels match one of these types, case-insensitive (e.g. "bug", "good first issue") (#168). Not a filter; only reorders.'
40135
+ ),
40095
40136
  diversityRatio: external_exports3.number().min(0).max(1).optional().describe(
40096
40137
  "Diversity counterweight (#1244): fraction of result slots reserved for candidates that matched NEITHER preference list. Counters echo-chamber bias when boosts accumulate over time. 0 disables, 1 reserves every slot for diversity. Default 0."
40097
40138
  )
@@ -40101,6 +40142,8 @@ function registerTools(server, scout) {
40101
40142
  strategies,
40102
40143
  preferLanguages,
40103
40144
  preferRepos,
40145
+ avoidRepos,
40146
+ boostIssueTypes,
40104
40147
  diversityRatio
40105
40148
  }) => {
40106
40149
  try {
@@ -40111,6 +40154,8 @@ function registerTools(server, scout) {
40111
40154
  strategies: parsedStrategies,
40112
40155
  preferLanguages,
40113
40156
  preferRepos,
40157
+ avoidRepos,
40158
+ boostIssueTypes,
40114
40159
  diversityRatio,
40115
40160
  // No fixed inter-phase sleeps in the request/response MCP context;
40116
40161
  // the budget tracker still paces the GitHub calls (#143)
@@ -40119,9 +40164,18 @@ function registerTools(server, scout) {
40119
40164
  })
40120
40165
  );
40121
40166
  scout.saveResults(result.candidates);
40122
- await scout.checkpoint();
40167
+ const persisted = await scout.checkpoint();
40123
40168
  return {
40124
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
40169
+ content: [
40170
+ {
40171
+ type: "text",
40172
+ text: JSON.stringify(
40173
+ withPersistWarning(result, persisted),
40174
+ null,
40175
+ 2
40176
+ )
40177
+ }
40178
+ ]
40125
40179
  };
40126
40180
  } catch (err) {
40127
40181
  const msg = err instanceof Error ? err.message : String(err);
@@ -40150,9 +40204,18 @@ function registerTools(server, scout) {
40150
40204
  })
40151
40205
  );
40152
40206
  scout.saveResults([...result.quickWins, ...result.biggerBets]);
40153
- await scout.checkpoint();
40207
+ const persisted = await scout.checkpoint();
40154
40208
  return {
40155
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
40209
+ content: [
40210
+ {
40211
+ type: "text",
40212
+ text: JSON.stringify(
40213
+ withPersistWarning(result, persisted),
40214
+ null,
40215
+ 2
40216
+ )
40217
+ }
40218
+ ]
40156
40219
  };
40157
40220
  } catch (err) {
40158
40221
  const msg = err instanceof Error ? err.message : String(err);
@@ -40324,10 +40387,19 @@ function registerTools(server, scout) {
40324
40387
  scout.updatePreferences({
40325
40388
  [key]: validated[key]
40326
40389
  });
40327
- await scout.checkpoint();
40390
+ const persisted = await scout.checkpoint();
40328
40391
  const updated = scout.getPreferences();
40329
40392
  return {
40330
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
40393
+ content: [
40394
+ {
40395
+ type: "text",
40396
+ text: JSON.stringify(
40397
+ withPersistWarning(updated, persisted),
40398
+ null,
40399
+ 2
40400
+ )
40401
+ }
40402
+ ]
40331
40403
  };
40332
40404
  }
40333
40405
  );
@@ -40385,7 +40457,7 @@ function registerResources(server, scout) {
40385
40457
  // src/index.ts
40386
40458
  var SERVER_INFO = {
40387
40459
  name: "oss-scout-mcp",
40388
- version: "0.9.0"
40460
+ version: "0.10.0"
40389
40461
  // x-release-please-version
40390
40462
  };
40391
40463
  function createServer(scout) {
package/dist/tools.js CHANGED
@@ -3,6 +3,19 @@ import { SearchStrategySchema, ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl
3
3
  // Generous ceiling: even with inter-phase delays zeroed, vetting many issues
4
4
  // under the budget tracker's sliding-window pacing can take minutes (#143).
5
5
  const TOOL_TIMEOUT_MS = 300000;
6
+ // Surfaced on the JSON envelope when scout.checkpoint() returns false (#212).
7
+ // The skip tool reports this inline because it returns a short status line;
8
+ // the JSON tools (search, scout-features, config-set) carry it as a top-level
9
+ // field so a persist failure isn't silently swallowed.
10
+ const PERSIST_WARNING = "Checkpoint failed: changes were applied in memory but could not be persisted. They may be lost when the server restarts.";
11
+ /**
12
+ * Serialize a tool result, attaching a top-level `persistWarning` field when
13
+ * the preceding checkpoint failed (#212). Spreads onto the existing object so
14
+ * the happy-path payload is byte-for-byte unchanged.
15
+ */
16
+ function withPersistWarning(payload, persisted) {
17
+ return persisted ? payload : { ...payload, persistWarning: PERSIST_WARNING };
18
+ }
6
19
  function withTimeout(promise, ms = TOOL_TIMEOUT_MS) {
7
20
  let timeoutId;
8
21
  const timer = new Promise((_, reject) => {
@@ -28,13 +41,21 @@ export function registerTools(server, scout) {
28
41
  .array(z.string())
29
42
  .optional()
30
43
  .describe("Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."),
44
+ avoidRepos: z
45
+ .array(z.string())
46
+ .optional()
47
+ .describe("Soft PENALTY (milder than excludeRepos) for candidates in one of these `owner/repo` slugs (#168). Pushes them down the ranking but does not remove them; a strong boost can still outweigh it."),
48
+ boostIssueTypes: z
49
+ .array(z.string())
50
+ .optional()
51
+ .describe('Soft-boost ranking for candidates whose issue labels match one of these types, case-insensitive (e.g. "bug", "good first issue") (#168). Not a filter; only reorders.'),
31
52
  diversityRatio: z
32
53
  .number()
33
54
  .min(0)
34
55
  .max(1)
35
56
  .optional()
36
57
  .describe("Diversity counterweight (#1244): fraction of result slots reserved for candidates that matched NEITHER preference list. Counters echo-chamber bias when boosts accumulate over time. 0 disables, 1 reserves every slot for diversity. Default 0."),
37
- }, async ({ maxResults, strategies, preferLanguages, preferRepos, diversityRatio, }) => {
58
+ }, async ({ maxResults, strategies, preferLanguages, preferRepos, avoidRepos, boostIssueTypes, diversityRatio, }) => {
38
59
  try {
39
60
  const parsedStrategies = strategies
40
61
  ? strategies
@@ -46,6 +67,8 @@ export function registerTools(server, scout) {
46
67
  strategies: parsedStrategies,
47
68
  preferLanguages,
48
69
  preferRepos,
70
+ avoidRepos,
71
+ boostIssueTypes,
49
72
  diversityRatio,
50
73
  // No fixed inter-phase sleeps in the request/response MCP context;
51
74
  // the budget tracker still paces the GitHub calls (#143)
@@ -53,9 +76,14 @@ export function registerTools(server, scout) {
53
76
  broadPhaseDelayMs: 0,
54
77
  }));
55
78
  scout.saveResults(result.candidates);
56
- await scout.checkpoint();
79
+ const persisted = await scout.checkpoint();
57
80
  return {
58
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: JSON.stringify(withPersistWarning(result, persisted), null, 2),
85
+ },
86
+ ],
59
87
  };
60
88
  }
61
89
  catch (err) {
@@ -92,9 +120,14 @@ export function registerTools(server, scout) {
92
120
  splitRatio,
93
121
  }));
94
122
  scout.saveResults([...result.quickWins, ...result.biggerBets]);
95
- await scout.checkpoint();
123
+ const persisted = await scout.checkpoint();
96
124
  return {
97
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: JSON.stringify(withPersistWarning(result, persisted), null, 2),
129
+ },
130
+ ],
98
131
  };
99
132
  }
100
133
  catch (err) {
@@ -279,10 +312,15 @@ export function registerTools(server, scout) {
279
312
  scout.updatePreferences({
280
313
  [key]: validated[key],
281
314
  });
282
- await scout.checkpoint();
315
+ const persisted = await scout.checkpoint();
283
316
  const updated = scout.getPreferences();
284
317
  return {
285
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
318
+ content: [
319
+ {
320
+ type: "text",
321
+ text: JSON.stringify(withPersistWarning(updated, persisted), null, 2),
322
+ },
323
+ ],
286
324
  };
287
325
  });
288
326
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.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,7 +18,7 @@
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.27.1",
20
20
  "zod": "^4.3.6",
21
- "@oss-scout/core": "^1.0.0"
21
+ "@oss-scout/core": "^1.1.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.5.0",