@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 +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-server.bundle.cjs +231 -159
- package/dist/tools.js +45 -7
- package/package.json +2 -2
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.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
36906
|
-
|
|
36907
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
37910
|
-
|
|
37911
|
-
|
|
37912
|
-
|
|
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
|
-
|
|
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 (
|
|
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)
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
38488
|
-
|
|
38489
|
-
|
|
38490
|
-
|
|
38491
|
-
|
|
38492
|
-
|
|
38493
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
38872
|
+
debug(MODULE13, "State pushed to gist");
|
|
38854
38873
|
return true;
|
|
38855
38874
|
} catch (err) {
|
|
38856
38875
|
rethrowIfFatal(err);
|
|
38857
|
-
warn(
|
|
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(
|
|
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(
|
|
38900
|
+
debug(MODULE13, `Cached gist gone (404): ${errorMessage(err)}`);
|
|
38882
38901
|
}
|
|
38883
|
-
debug(
|
|
38902
|
+
debug(MODULE13, "Cached gist ID invalid, searching...");
|
|
38884
38903
|
}
|
|
38885
38904
|
const search = await this.searchForGist();
|
|
38886
38905
|
if (search.id) {
|
|
38887
|
-
debug(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
39190
|
+
warn(MODULE14, `Corrupt state backed up to ${backupPath}`);
|
|
39172
39191
|
} catch (backupErr) {
|
|
39173
|
-
warn(
|
|
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(
|
|
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
|
-
* `
|
|
39344
|
-
*
|
|
39345
|
-
*
|
|
39346
|
-
*
|
|
39347
|
-
*
|
|
39348
|
-
*
|
|
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
|
|
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
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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.
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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.
|
|
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.
|
|
21
|
+
"@oss-scout/core": "^1.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^25.5.0",
|