@oss-scout/mcp 0.8.0 → 0.9.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/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +35 -14
- package/dist/mcp-server.bundle.cjs +1258 -580
- package/dist/tools.js +51 -102
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
"use strict";
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
@@ -28888,13 +28889,13 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
28888
28889
|
}, true) ?? parseAnyDef(refs)
|
|
28889
28890
|
}), {}) : void 0;
|
|
28890
28891
|
const name = typeof options === "string" ? options : options?.nameStrategy === "title" ? void 0 : options?.name;
|
|
28891
|
-
const
|
|
28892
|
+
const main = parseDef(schema._def, name === void 0 ? refs : {
|
|
28892
28893
|
...refs,
|
|
28893
28894
|
currentPath: [...refs.basePath, refs.definitionPath, name]
|
|
28894
28895
|
}, false) ?? parseAnyDef(refs);
|
|
28895
28896
|
const title = typeof options === "object" && options.name !== void 0 && options.nameStrategy === "title" ? options.name : void 0;
|
|
28896
28897
|
if (title !== void 0) {
|
|
28897
|
-
|
|
28898
|
+
main.title = title;
|
|
28898
28899
|
}
|
|
28899
28900
|
if (refs.flags.hasReferencedOpenAiAnyType) {
|
|
28900
28901
|
if (!definitions) {
|
|
@@ -28915,9 +28916,9 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
28915
28916
|
}
|
|
28916
28917
|
}
|
|
28917
28918
|
const combined = name === void 0 ? definitions ? {
|
|
28918
|
-
...
|
|
28919
|
+
...main,
|
|
28919
28920
|
[refs.definitionPath]: definitions
|
|
28920
|
-
} :
|
|
28921
|
+
} : main : {
|
|
28921
28922
|
$ref: [
|
|
28922
28923
|
...refs.$refStrategy === "relative" ? [] : refs.basePath,
|
|
28923
28924
|
refs.definitionPath,
|
|
@@ -28925,7 +28926,7 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
28925
28926
|
].join("/"),
|
|
28926
28927
|
[refs.definitionPath]: {
|
|
28927
28928
|
...definitions,
|
|
28928
|
-
[name]:
|
|
28929
|
+
[name]: main
|
|
28929
28930
|
}
|
|
28930
28931
|
};
|
|
28931
28932
|
if (refs.target === "jsonSchema7") {
|
|
@@ -30262,8 +30263,8 @@ var Server = class extends Protocol {
|
|
|
30262
30263
|
this._loggingLevels = /* @__PURE__ */ new Map();
|
|
30263
30264
|
this.LOG_LEVEL_SEVERITY = new Map(LoggingLevelSchema.options.map((level, index) => [level, index]));
|
|
30264
30265
|
this.isMessageIgnored = (level, sessionId) => {
|
|
30265
|
-
const
|
|
30266
|
-
return
|
|
30266
|
+
const currentLevel2 = this._loggingLevels.get(sessionId);
|
|
30267
|
+
return currentLevel2 ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel2) : false;
|
|
30267
30268
|
};
|
|
30268
30269
|
this._capabilities = options?.capabilities ?? {};
|
|
30269
30270
|
this._instructions = options?.instructions;
|
|
@@ -35449,18 +35450,34 @@ throttling.VERSION = VERSION9;
|
|
|
35449
35450
|
throttling.triggersNotification = triggersNotification;
|
|
35450
35451
|
|
|
35451
35452
|
// ../core/dist/core/logger.js
|
|
35452
|
-
var
|
|
35453
|
+
var LEVEL_RANK = {
|
|
35454
|
+
silent: 0,
|
|
35455
|
+
warn: 1,
|
|
35456
|
+
info: 2,
|
|
35457
|
+
debug: 3
|
|
35458
|
+
};
|
|
35459
|
+
var currentLevel = "info";
|
|
35460
|
+
function setLogLevel(level) {
|
|
35461
|
+
currentLevel = level;
|
|
35462
|
+
}
|
|
35463
|
+
function shouldLog(level) {
|
|
35464
|
+
return LEVEL_RANK[currentLevel] >= LEVEL_RANK[level];
|
|
35465
|
+
}
|
|
35453
35466
|
function debug(module2, message, ...args) {
|
|
35454
|
-
if (!
|
|
35467
|
+
if (!shouldLog("debug"))
|
|
35455
35468
|
return;
|
|
35456
35469
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
35457
35470
|
console.error(`[${timestamp}] [DEBUG] [${module2}] ${message}`, ...args);
|
|
35458
35471
|
}
|
|
35459
35472
|
function info(module2, message, ...args) {
|
|
35473
|
+
if (!shouldLog("info"))
|
|
35474
|
+
return;
|
|
35460
35475
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
35461
35476
|
console.error(`[${timestamp}] [INFO] [${module2}] ${message}`, ...args);
|
|
35462
35477
|
}
|
|
35463
35478
|
function warn(module2, message, ...args) {
|
|
35479
|
+
if (!shouldLog("warn"))
|
|
35480
|
+
return;
|
|
35464
35481
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
35465
35482
|
console.error(`[${timestamp}] [WARN] [${module2}] ${message}`, ...args);
|
|
35466
35483
|
}
|
|
@@ -35556,10 +35573,15 @@ function isRateLimitError(error48) {
|
|
|
35556
35573
|
return true;
|
|
35557
35574
|
if (status === 403) {
|
|
35558
35575
|
const msg = errorMessage(error48).toLowerCase();
|
|
35559
|
-
return msg.includes("rate limit");
|
|
35576
|
+
return msg.includes("rate limit") || msg.includes("abuse detection");
|
|
35560
35577
|
}
|
|
35561
35578
|
return false;
|
|
35562
35579
|
}
|
|
35580
|
+
function rethrowIfFatal(error48) {
|
|
35581
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
35582
|
+
throw error48;
|
|
35583
|
+
}
|
|
35584
|
+
}
|
|
35563
35585
|
|
|
35564
35586
|
// ../core/dist/core/utils.js
|
|
35565
35587
|
function sleep(ms) {
|
|
@@ -35583,12 +35605,23 @@ function getCacheDir() {
|
|
|
35583
35605
|
return dir;
|
|
35584
35606
|
}
|
|
35585
35607
|
function extractRepoFromUrl(url2) {
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35590
|
-
|
|
35591
|
-
|
|
35608
|
+
let parsed;
|
|
35609
|
+
try {
|
|
35610
|
+
parsed = new URL(url2);
|
|
35611
|
+
} catch {
|
|
35612
|
+
return null;
|
|
35613
|
+
}
|
|
35614
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
35615
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
35616
|
+
if (host === "api.github.com") {
|
|
35617
|
+
if (segments[0] === "repos" && segments.length >= 3) {
|
|
35618
|
+
return `${segments[1]}/${segments[2]}`;
|
|
35619
|
+
}
|
|
35620
|
+
return null;
|
|
35621
|
+
}
|
|
35622
|
+
if (host === "github.com" && segments.length >= 2) {
|
|
35623
|
+
return `${segments[0]}/${segments[1]}`;
|
|
35624
|
+
}
|
|
35592
35625
|
return null;
|
|
35593
35626
|
}
|
|
35594
35627
|
var OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
@@ -35597,25 +35630,29 @@ function isValidOwnerRepo(owner, repo) {
|
|
|
35597
35630
|
return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
|
|
35598
35631
|
}
|
|
35599
35632
|
function parseGitHubUrl(url2) {
|
|
35600
|
-
|
|
35633
|
+
const normalized = /^(?:www\.)?github\.com\//i.test(url2) ? `https://${url2}` : url2;
|
|
35634
|
+
let parsed;
|
|
35635
|
+
try {
|
|
35636
|
+
parsed = new URL(normalized);
|
|
35637
|
+
} catch {
|
|
35601
35638
|
return null;
|
|
35602
|
-
const prMatch = url2.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
35603
|
-
if (prMatch) {
|
|
35604
|
-
const owner = prMatch[1];
|
|
35605
|
-
const repo = prMatch[2];
|
|
35606
|
-
if (!isValidOwnerRepo(owner, repo))
|
|
35607
|
-
return null;
|
|
35608
|
-
return { owner, repo, number: parseInt(prMatch[3], 10), type: "pull" };
|
|
35609
|
-
}
|
|
35610
|
-
const issueMatch = url2.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
35611
|
-
if (issueMatch) {
|
|
35612
|
-
const owner = issueMatch[1];
|
|
35613
|
-
const repo = issueMatch[2];
|
|
35614
|
-
if (!isValidOwnerRepo(owner, repo))
|
|
35615
|
-
return null;
|
|
35616
|
-
return { owner, repo, number: parseInt(issueMatch[3], 10), type: "issues" };
|
|
35617
35639
|
}
|
|
35618
|
-
|
|
35640
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
35641
|
+
return null;
|
|
35642
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
35643
|
+
if (host !== "github.com")
|
|
35644
|
+
return null;
|
|
35645
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
35646
|
+
if (segments.length !== 4)
|
|
35647
|
+
return null;
|
|
35648
|
+
const [owner, repo, type, num] = segments;
|
|
35649
|
+
if (type !== "pull" && type !== "issues")
|
|
35650
|
+
return null;
|
|
35651
|
+
if (!isValidOwnerRepo(owner, repo))
|
|
35652
|
+
return null;
|
|
35653
|
+
if (!/^\d+$/.test(num))
|
|
35654
|
+
return null;
|
|
35655
|
+
return { owner, repo, number: parseInt(num, 10), type };
|
|
35619
35656
|
}
|
|
35620
35657
|
function daysBetween(from, to = /* @__PURE__ */ new Date()) {
|
|
35621
35658
|
return Math.max(0, Math.floor((to.getTime() - from.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
@@ -35626,8 +35663,9 @@ function getGitHubToken() {
|
|
|
35626
35663
|
if (tokenFetchAttempted)
|
|
35627
35664
|
return null;
|
|
35628
35665
|
tokenFetchAttempted = true;
|
|
35629
|
-
|
|
35630
|
-
|
|
35666
|
+
const envToken = process.env.GITHUB_TOKEN?.trim();
|
|
35667
|
+
if (envToken) {
|
|
35668
|
+
cachedGitHubToken = envToken;
|
|
35631
35669
|
return cachedGitHubToken;
|
|
35632
35670
|
}
|
|
35633
35671
|
try {
|
|
@@ -35642,7 +35680,7 @@ function getGitHubToken() {
|
|
|
35642
35680
|
return cachedGitHubToken;
|
|
35643
35681
|
}
|
|
35644
35682
|
} catch (err) {
|
|
35645
|
-
debug(MODULE2,
|
|
35683
|
+
debug(MODULE2, `gh auth token failed: ${errorMessage(err)}`);
|
|
35646
35684
|
}
|
|
35647
35685
|
return null;
|
|
35648
35686
|
}
|
|
@@ -35662,6 +35700,8 @@ var SearchBudgetTracker = class {
|
|
|
35662
35700
|
resetAt = 0;
|
|
35663
35701
|
/** Total calls recorded since init (for diagnostics). */
|
|
35664
35702
|
totalCalls = 0;
|
|
35703
|
+
/** Calls made since the last known quota reset (external accounting). */
|
|
35704
|
+
callsSinceReset = 0;
|
|
35665
35705
|
/**
|
|
35666
35706
|
* Initialize with pre-flight rate limit data from GitHub.
|
|
35667
35707
|
*/
|
|
@@ -35670,6 +35710,7 @@ var SearchBudgetTracker = class {
|
|
|
35670
35710
|
this.resetAt = new Date(resetAt).getTime();
|
|
35671
35711
|
this.callTimestamps = [];
|
|
35672
35712
|
this.totalCalls = 0;
|
|
35713
|
+
this.callsSinceReset = 0;
|
|
35673
35714
|
debug(MODULE3, `Initialized: ${remaining} remaining, resets at ${new Date(this.resetAt).toLocaleTimeString()}`);
|
|
35674
35715
|
}
|
|
35675
35716
|
/**
|
|
@@ -35678,8 +35719,28 @@ var SearchBudgetTracker = class {
|
|
|
35678
35719
|
recordCall() {
|
|
35679
35720
|
this.callTimestamps.push(Date.now());
|
|
35680
35721
|
this.totalCalls++;
|
|
35722
|
+
this.callsSinceReset++;
|
|
35681
35723
|
this.pruneOldTimestamps();
|
|
35682
35724
|
}
|
|
35725
|
+
/**
|
|
35726
|
+
* Replenish the external budget once GitHub's quota window has reset.
|
|
35727
|
+
* Without this, the pre-flight remaining acted as a never-replenishing
|
|
35728
|
+
* run-lifetime total, throttling long multi-phase runs to ~1 call/min
|
|
35729
|
+
* even though GitHub fully resets every 60 seconds (#119).
|
|
35730
|
+
*/
|
|
35731
|
+
refreshExternalBudget() {
|
|
35732
|
+
if (this.resetAt <= 0)
|
|
35733
|
+
return;
|
|
35734
|
+
const now = Date.now();
|
|
35735
|
+
if (now < this.resetAt)
|
|
35736
|
+
return;
|
|
35737
|
+
this.knownRemaining = SEARCH_RATE_LIMIT;
|
|
35738
|
+
this.callsSinceReset = 0;
|
|
35739
|
+
while (this.resetAt <= now) {
|
|
35740
|
+
this.resetAt += SEARCH_WINDOW_MS;
|
|
35741
|
+
}
|
|
35742
|
+
debug(MODULE3, "Quota window reset; external search budget replenished");
|
|
35743
|
+
}
|
|
35683
35744
|
/**
|
|
35684
35745
|
* Remove timestamps older than the sliding window.
|
|
35685
35746
|
*/
|
|
@@ -35701,8 +35762,9 @@ var SearchBudgetTracker = class {
|
|
|
35701
35762
|
* and the pre-flight remaining quota from GitHub.
|
|
35702
35763
|
*/
|
|
35703
35764
|
getEffectiveBudget() {
|
|
35765
|
+
this.refreshExternalBudget();
|
|
35704
35766
|
const localBudget = EFFECTIVE_BUDGET - this.callTimestamps.length;
|
|
35705
|
-
const externalBudget = this.knownRemaining - this.
|
|
35767
|
+
const externalBudget = this.knownRemaining - this.callsSinceReset;
|
|
35706
35768
|
return Math.max(0, Math.min(localBudget, externalBudget));
|
|
35707
35769
|
}
|
|
35708
35770
|
/**
|
|
@@ -35725,6 +35787,12 @@ var SearchBudgetTracker = class {
|
|
|
35725
35787
|
}
|
|
35726
35788
|
const oldestInWindow = this.callTimestamps[0];
|
|
35727
35789
|
if (!oldestInWindow) {
|
|
35790
|
+
const untilReset = this.resetAt - Date.now();
|
|
35791
|
+
if (this.resetAt > 0 && untilReset > 0) {
|
|
35792
|
+
debug(MODULE3, `External quota exhausted, waiting ${untilReset}ms for reset`);
|
|
35793
|
+
await sleep(untilReset + 100);
|
|
35794
|
+
continue;
|
|
35795
|
+
}
|
|
35728
35796
|
return;
|
|
35729
35797
|
}
|
|
35730
35798
|
const waitUntil = oldestInWindow + SEARCH_WINDOW_MS;
|
|
@@ -35770,12 +35838,7 @@ var SCOPE_LABELS = {
|
|
|
35770
35838
|
};
|
|
35771
35839
|
|
|
35772
35840
|
// ../core/dist/core/schemas.js
|
|
35773
|
-
var IssueStatusSchema = external_exports3.enum([
|
|
35774
|
-
"candidate",
|
|
35775
|
-
"claimed",
|
|
35776
|
-
"in_progress",
|
|
35777
|
-
"pr_submitted"
|
|
35778
|
-
]);
|
|
35841
|
+
var IssueStatusSchema = external_exports3.enum(["candidate"]);
|
|
35779
35842
|
var ProjectCategorySchema = external_exports3.enum([
|
|
35780
35843
|
"nonprofit",
|
|
35781
35844
|
"devtools",
|
|
@@ -35802,12 +35865,12 @@ var CONCRETE_STRATEGIES = [
|
|
|
35802
35865
|
"broad",
|
|
35803
35866
|
"maintained"
|
|
35804
35867
|
];
|
|
35805
|
-
var RepoSignalsSchema = external_exports3.
|
|
35868
|
+
var RepoSignalsSchema = external_exports3.looseObject({
|
|
35806
35869
|
hasActiveMaintainers: external_exports3.boolean(),
|
|
35807
35870
|
isResponsive: external_exports3.boolean(),
|
|
35808
35871
|
hasHostileComments: external_exports3.boolean()
|
|
35809
35872
|
});
|
|
35810
|
-
var RepoScoreSchema = external_exports3.
|
|
35873
|
+
var RepoScoreSchema = external_exports3.looseObject({
|
|
35811
35874
|
repo: external_exports3.string(),
|
|
35812
35875
|
score: external_exports3.number(),
|
|
35813
35876
|
mergedPRCount: external_exports3.number(),
|
|
@@ -35819,17 +35882,17 @@ var RepoScoreSchema = external_exports3.object({
|
|
|
35819
35882
|
stargazersCount: external_exports3.number().optional(),
|
|
35820
35883
|
language: external_exports3.string().nullable().optional()
|
|
35821
35884
|
});
|
|
35822
|
-
var StoredMergedPRSchema = external_exports3.
|
|
35885
|
+
var StoredMergedPRSchema = external_exports3.looseObject({
|
|
35823
35886
|
url: external_exports3.string(),
|
|
35824
35887
|
title: external_exports3.string(),
|
|
35825
35888
|
mergedAt: external_exports3.string()
|
|
35826
35889
|
});
|
|
35827
|
-
var StoredClosedPRSchema = external_exports3.
|
|
35890
|
+
var StoredClosedPRSchema = external_exports3.looseObject({
|
|
35828
35891
|
url: external_exports3.string(),
|
|
35829
35892
|
title: external_exports3.string(),
|
|
35830
35893
|
closedAt: external_exports3.string()
|
|
35831
35894
|
});
|
|
35832
|
-
var StoredOpenPRSchema = external_exports3.
|
|
35895
|
+
var StoredOpenPRSchema = external_exports3.looseObject({
|
|
35833
35896
|
url: external_exports3.string(),
|
|
35834
35897
|
title: external_exports3.string(),
|
|
35835
35898
|
openedAt: external_exports3.string()
|
|
@@ -35889,7 +35952,7 @@ var TrackedIssueSchema = external_exports3.object({
|
|
|
35889
35952
|
vetted: external_exports3.boolean(),
|
|
35890
35953
|
vettingResult: IssueVettingResultSchema.optional()
|
|
35891
35954
|
});
|
|
35892
|
-
var SkippedIssueSchema = external_exports3.
|
|
35955
|
+
var SkippedIssueSchema = external_exports3.looseObject({
|
|
35893
35956
|
url: external_exports3.string(),
|
|
35894
35957
|
repo: external_exports3.string(),
|
|
35895
35958
|
number: external_exports3.number(),
|
|
@@ -35897,7 +35960,7 @@ var SkippedIssueSchema = external_exports3.object({
|
|
|
35897
35960
|
skippedAt: external_exports3.string()
|
|
35898
35961
|
});
|
|
35899
35962
|
var HorizonSchema = external_exports3.enum(["quick-win", "bigger-bet"]);
|
|
35900
|
-
var SavedCandidateSchema = external_exports3.
|
|
35963
|
+
var SavedCandidateSchema = external_exports3.looseObject({
|
|
35901
35964
|
issueUrl: external_exports3.string(),
|
|
35902
35965
|
repo: external_exports3.string(),
|
|
35903
35966
|
number: external_exports3.number(),
|
|
@@ -35905,14 +35968,23 @@ var SavedCandidateSchema = external_exports3.object({
|
|
|
35905
35968
|
labels: external_exports3.array(external_exports3.string()),
|
|
35906
35969
|
recommendation: external_exports3.enum(["approve", "skip", "needs_review"]),
|
|
35907
35970
|
viabilityScore: external_exports3.number(),
|
|
35908
|
-
|
|
35971
|
+
// Tightened from z.string() to the actual 3-value union (#158). `.catch`
|
|
35972
|
+
// keeps old persisted state loadable: any unrecognized stored value (or a
|
|
35973
|
+
// future-version value) decodes to "normal" instead of failing the parse.
|
|
35974
|
+
searchPriority: external_exports3.enum(["merged_pr", "starred", "normal"]).catch("normal"),
|
|
35909
35975
|
firstSeenAt: external_exports3.string(),
|
|
35910
35976
|
lastSeenAt: external_exports3.string(),
|
|
35911
35977
|
lastScore: external_exports3.number(),
|
|
35912
|
-
horizon: HorizonSchema.optional()
|
|
35978
|
+
horizon: HorizonSchema.optional(),
|
|
35979
|
+
/**
|
|
35980
|
+
* Availability status recorded by the previous `vet-list` run, so the next
|
|
35981
|
+
* run can report transitions ("was available, now claimed") instead of
|
|
35982
|
+
* re-diffing full snapshots (#165). "error" is never stored.
|
|
35983
|
+
*/
|
|
35984
|
+
lastStatus: external_exports3.enum(["still_available", "claimed", "closed", "has_pr"]).optional()
|
|
35913
35985
|
});
|
|
35914
35986
|
var PersistenceModeSchema = external_exports3.enum(["local", "gist"]);
|
|
35915
|
-
var ScoutPreferencesSchema = external_exports3.
|
|
35987
|
+
var ScoutPreferencesSchema = external_exports3.looseObject({
|
|
35916
35988
|
githubUsername: external_exports3.string().default(""),
|
|
35917
35989
|
languages: external_exports3.array(external_exports3.string()).default(["any"]),
|
|
35918
35990
|
labels: external_exports3.array(external_exports3.string()).default(["good first issue", "help wanted"]),
|
|
@@ -35928,8 +36000,22 @@ var ScoutPreferencesSchema = external_exports3.object({
|
|
|
35928
36000
|
interPhaseDelayMs: external_exports3.number().min(0).max(12e4).default(3e4),
|
|
35929
36001
|
persistence: PersistenceModeSchema.default("local"),
|
|
35930
36002
|
defaultStrategy: external_exports3.array(SearchStrategySchema).optional(),
|
|
36003
|
+
/**
|
|
36004
|
+
* Persisted personalization defaults (#168). The `--prefer-languages`,
|
|
36005
|
+
* `--prefer-repos`, and `--diversity-ratio` search flags override these when
|
|
36006
|
+
* passed, so a stored preference removes the need to retype the boost every
|
|
36007
|
+
* search. Empty / 0 disables the corresponding signal (same as the flags).
|
|
36008
|
+
*/
|
|
36009
|
+
preferLanguages: external_exports3.array(external_exports3.string()).default([]),
|
|
36010
|
+
preferRepos: external_exports3.array(external_exports3.string()).default([]),
|
|
36011
|
+
diversityRatio: external_exports3.number().min(0).max(1).default(0),
|
|
35931
36012
|
broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
|
|
35932
|
-
|
|
36013
|
+
/**
|
|
36014
|
+
* Skip the expensive broad phase once this many candidates were found by
|
|
36015
|
+
* the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
|
|
36016
|
+
* satisfiable; 0 disables skipping entirely.
|
|
36017
|
+
*/
|
|
36018
|
+
skipBroadWhenSufficientResults: external_exports3.number().int().min(0).max(100).default(8),
|
|
35933
36019
|
/**
|
|
35934
36020
|
* Optional Ollama model id used for SLM pre-triage during vetting
|
|
35935
36021
|
* (oss-autopilot#1122). Empty disables the feature. Recommended values:
|
|
@@ -35956,7 +36042,7 @@ var ScoutPreferencesSchema = external_exports3.object({
|
|
|
35956
36042
|
*/
|
|
35957
36043
|
featuresSplitRatio: external_exports3.number().min(0).max(1).default(0.6)
|
|
35958
36044
|
});
|
|
35959
|
-
var ScoutStateSchema = external_exports3.
|
|
36045
|
+
var ScoutStateSchema = external_exports3.looseObject({
|
|
35960
36046
|
version: external_exports3.literal(1),
|
|
35961
36047
|
preferences: ScoutPreferencesSchema.default(() => ScoutPreferencesSchema.parse({})),
|
|
35962
36048
|
repoScores: external_exports3.record(external_exports3.string(), RepoScoreSchema).default({}),
|
|
@@ -35967,10 +36053,30 @@ var ScoutStateSchema = external_exports3.object({
|
|
|
35967
36053
|
openPRs: external_exports3.array(StoredOpenPRSchema).default([]),
|
|
35968
36054
|
savedResults: external_exports3.array(SavedCandidateSchema).default([]),
|
|
35969
36055
|
skippedIssues: external_exports3.array(SkippedIssueSchema).default([]),
|
|
36056
|
+
/**
|
|
36057
|
+
* Deletion tombstones (#117). Without these, the gist union-merge would
|
|
36058
|
+
* resurrect any saved result or skipped issue removed on one machine the
|
|
36059
|
+
* next time it merged against another machine's copy that still had it.
|
|
36060
|
+
* A tombstone suppresses an item across a merge until the item is
|
|
36061
|
+
* re-added with a newer timestamp. Pruned after 90 days.
|
|
36062
|
+
*/
|
|
36063
|
+
tombstones: external_exports3.array(external_exports3.looseObject({
|
|
36064
|
+
url: external_exports3.string(),
|
|
36065
|
+
removedAt: external_exports3.string()
|
|
36066
|
+
})).default([]),
|
|
36067
|
+
/**
|
|
36068
|
+
* When preferences were last changed (#117). The gist merge previously
|
|
36069
|
+
* always took the remote preferences, silently reverting a local edit;
|
|
36070
|
+
* the side with the fresher timestamp now wins.
|
|
36071
|
+
*/
|
|
36072
|
+
preferencesUpdatedAt: external_exports3.string().optional(),
|
|
35970
36073
|
lastSearchAt: external_exports3.string().optional(),
|
|
35971
36074
|
lastRunAt: external_exports3.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
35972
36075
|
gistId: external_exports3.string().optional()
|
|
35973
36076
|
});
|
|
36077
|
+
function parseScoutState(raw) {
|
|
36078
|
+
return ScoutStateSchema.parse(raw);
|
|
36079
|
+
}
|
|
35974
36080
|
|
|
35975
36081
|
// ../core/dist/core/issue-filtering.js
|
|
35976
36082
|
var DOC_ONLY_LABELS = /* @__PURE__ */ new Set([
|
|
@@ -36268,6 +36374,10 @@ var fs2 = __toESM(require("fs"), 1);
|
|
|
36268
36374
|
var path2 = __toESM(require("path"), 1);
|
|
36269
36375
|
var crypto = __toESM(require("crypto"), 1);
|
|
36270
36376
|
var MODULE4 = "http-cache";
|
|
36377
|
+
var CACHE_SCHEMA_VERSION = "v1";
|
|
36378
|
+
function versionedCacheKey(key) {
|
|
36379
|
+
return `${CACHE_SCHEMA_VERSION}:${key}`;
|
|
36380
|
+
}
|
|
36271
36381
|
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
36272
36382
|
var HttpCache = class {
|
|
36273
36383
|
cacheDir;
|
|
@@ -36290,13 +36400,21 @@ var HttpCache = class {
|
|
|
36290
36400
|
* (e.g., caching aggregated results from paginated API calls).
|
|
36291
36401
|
*/
|
|
36292
36402
|
getIfFresh(key, maxAgeMs) {
|
|
36403
|
+
return this.getEntryIfFresh(key, maxAgeMs)?.body ?? null;
|
|
36404
|
+
}
|
|
36405
|
+
/**
|
|
36406
|
+
* Like {@link getIfFresh}, but returns the whole entry so callers can
|
|
36407
|
+
* distinguish "no fresh entry" (null) from a legitimately cached falsy
|
|
36408
|
+
* body (`0`, `""`, `false`, `null`).
|
|
36409
|
+
*/
|
|
36410
|
+
getEntryIfFresh(key, maxAgeMs) {
|
|
36293
36411
|
const entry = this.get(key);
|
|
36294
36412
|
if (!entry)
|
|
36295
36413
|
return null;
|
|
36296
36414
|
const age = Date.now() - new Date(entry.cachedAt).getTime();
|
|
36297
36415
|
if (!Number.isFinite(age) || age < 0 || age > maxAgeMs)
|
|
36298
36416
|
return null;
|
|
36299
|
-
return entry
|
|
36417
|
+
return entry;
|
|
36300
36418
|
}
|
|
36301
36419
|
/**
|
|
36302
36420
|
* Look up a cached response. Returns `null` if no cache entry exists.
|
|
@@ -36447,12 +36565,21 @@ function getHttpCache() {
|
|
|
36447
36565
|
}
|
|
36448
36566
|
return _httpCache;
|
|
36449
36567
|
}
|
|
36450
|
-
async function
|
|
36451
|
-
const existing = cache.getInflight(
|
|
36568
|
+
async function withInflightDedup(cache, key, fn) {
|
|
36569
|
+
const existing = cache.getInflight(key);
|
|
36452
36570
|
if (existing) {
|
|
36453
|
-
debug(MODULE4, `Dedup hit for ${
|
|
36571
|
+
debug(MODULE4, `Dedup hit for ${key}`);
|
|
36454
36572
|
return await existing;
|
|
36455
36573
|
}
|
|
36574
|
+
const promise2 = fn();
|
|
36575
|
+
const cleanup = cache.setInflight(key, promise2);
|
|
36576
|
+
try {
|
|
36577
|
+
return await promise2;
|
|
36578
|
+
} finally {
|
|
36579
|
+
cleanup();
|
|
36580
|
+
}
|
|
36581
|
+
}
|
|
36582
|
+
async function cachedRequest(cache, url2, fetcher) {
|
|
36456
36583
|
const doFetch = async () => {
|
|
36457
36584
|
const extraHeaders = {};
|
|
36458
36585
|
const cached2 = cache.get(url2);
|
|
@@ -36473,28 +36600,34 @@ async function cachedRequest(cache, url2, fetcher) {
|
|
|
36473
36600
|
debug(MODULE4, `304 cache hit for ${url2}`);
|
|
36474
36601
|
return freshCached.body;
|
|
36475
36602
|
}
|
|
36603
|
+
debug(MODULE4, `304 but cache entry vanished for ${url2}, refetching`);
|
|
36604
|
+
const response = await fetcher({});
|
|
36605
|
+
const etag = response.headers?.["etag"];
|
|
36606
|
+
if (etag) {
|
|
36607
|
+
cache.set(url2, etag, response.data);
|
|
36608
|
+
}
|
|
36609
|
+
return response.data;
|
|
36476
36610
|
}
|
|
36477
36611
|
throw err;
|
|
36478
36612
|
}
|
|
36479
36613
|
};
|
|
36480
|
-
|
|
36481
|
-
const cleanup = cache.setInflight(url2, promise2);
|
|
36482
|
-
try {
|
|
36483
|
-
const result = await promise2;
|
|
36484
|
-
return result;
|
|
36485
|
-
} finally {
|
|
36486
|
-
cleanup();
|
|
36487
|
-
}
|
|
36614
|
+
return withInflightDedup(cache, url2, doFetch);
|
|
36488
36615
|
}
|
|
36489
36616
|
async function cachedTimeBased(cache, key, maxAgeMs, fetcher) {
|
|
36490
|
-
const cached2 = cache.
|
|
36617
|
+
const cached2 = cache.getEntryIfFresh(key, maxAgeMs);
|
|
36491
36618
|
if (cached2) {
|
|
36492
36619
|
debug(MODULE4, `Time-based cache hit for ${key}`);
|
|
36493
|
-
return cached2;
|
|
36620
|
+
return cached2.body;
|
|
36494
36621
|
}
|
|
36495
|
-
|
|
36496
|
-
|
|
36497
|
-
|
|
36622
|
+
return withInflightDedup(cache, key, async () => {
|
|
36623
|
+
const fresh = cache.getEntryIfFresh(key, maxAgeMs);
|
|
36624
|
+
if (fresh) {
|
|
36625
|
+
return fresh.body;
|
|
36626
|
+
}
|
|
36627
|
+
const result = await fetcher();
|
|
36628
|
+
cache.set(key, "", result);
|
|
36629
|
+
return result;
|
|
36630
|
+
});
|
|
36498
36631
|
}
|
|
36499
36632
|
function isNotModifiedError(err) {
|
|
36500
36633
|
return getHttpStatusCode(err) === 304;
|
|
@@ -36531,23 +36664,36 @@ function buildLinkedPRFromTimelineEvent(e, context) {
|
|
|
36531
36664
|
};
|
|
36532
36665
|
}
|
|
36533
36666
|
var MODULE5 = "issue-eligibility";
|
|
36534
|
-
var
|
|
36535
|
-
|
|
36536
|
-
|
|
36537
|
-
|
|
36538
|
-
|
|
36539
|
-
|
|
36540
|
-
|
|
36541
|
-
|
|
36542
|
-
|
|
36543
|
-
|
|
36544
|
-
|
|
36545
|
-
"i'm on it",
|
|
36546
|
-
"i'll submit a pr",
|
|
36547
|
-
"i will submit a pr",
|
|
36548
|
-
"working on a fix",
|
|
36549
|
-
"working on a pr"
|
|
36667
|
+
var ISSUE_OBJECT = String.raw`(?:this\b|it\b|that\b|the\b|#\d+|issue\b)`;
|
|
36668
|
+
var FIRST_PERSON_CLAIM_PATTERNS = [
|
|
36669
|
+
new RegExp(String.raw`\bi\s*(?:'ll|will) take ${ISSUE_OBJECT}`),
|
|
36670
|
+
new RegExp(String.raw`\bi\s*(?:'d|would) (?:like|love) to work on ${ISSUE_OBJECT}`),
|
|
36671
|
+
/\bi\s*(?:'m|am) on it\b/,
|
|
36672
|
+
/\bi\s*(?:'ll|will) submit a pr\b/,
|
|
36673
|
+
/\bassigned to me\b/
|
|
36674
|
+
];
|
|
36675
|
+
var GENERIC_WORKING_PATTERNS = [
|
|
36676
|
+
/\bworking on (?:this|it)\b/,
|
|
36677
|
+
/\bworking on a (?:fix|pr)\b/
|
|
36550
36678
|
];
|
|
36679
|
+
var PERMISSION_CLAIM_PATTERN = new RegExp(String.raw`\b(?:can|may|could) i (?:work on|take) ${ISSUE_OBJECT}`);
|
|
36680
|
+
var NON_CLAIM_SUBJECTS = /\b(?:anyone|anybody|someone|somebody|who|whoever|nobody|no[- ]?one|not)\b/;
|
|
36681
|
+
function commentClaimsIssue(body) {
|
|
36682
|
+
const clauses = body.toLowerCase().split(/(?<=[.!?])|\n+/);
|
|
36683
|
+
for (const clause of clauses) {
|
|
36684
|
+
if (PERMISSION_CLAIM_PATTERN.test(clause))
|
|
36685
|
+
return true;
|
|
36686
|
+
if (clause.trimEnd().endsWith("?"))
|
|
36687
|
+
continue;
|
|
36688
|
+
if (FIRST_PERSON_CLAIM_PATTERNS.some((p) => p.test(clause)))
|
|
36689
|
+
return true;
|
|
36690
|
+
if (NON_CLAIM_SUBJECTS.test(clause))
|
|
36691
|
+
continue;
|
|
36692
|
+
if (GENERIC_WORKING_PATTERNS.some((p) => p.test(clause)))
|
|
36693
|
+
return true;
|
|
36694
|
+
}
|
|
36695
|
+
return false;
|
|
36696
|
+
}
|
|
36551
36697
|
async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
36552
36698
|
try {
|
|
36553
36699
|
const timeline = await paginateAll((page) => octokit.issues.listEventsForTimeline({
|
|
@@ -36572,9 +36718,7 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
|
36572
36718
|
}
|
|
36573
36719
|
return { passed: linkedPRCount === 0, linkedPR };
|
|
36574
36720
|
} catch (error48) {
|
|
36575
|
-
|
|
36576
|
-
throw error48;
|
|
36577
|
-
}
|
|
36721
|
+
rethrowIfFatal(error48);
|
|
36578
36722
|
const errMsg = errorMessage(error48);
|
|
36579
36723
|
warn(MODULE5, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
36580
36724
|
return { passed: true, inconclusive: true, reason: errMsg, linkedPR: null };
|
|
@@ -36583,56 +36727,60 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
|
36583
36727
|
var MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
36584
36728
|
async function checkUserMergedPRsInRepo(octokit, owner, repo) {
|
|
36585
36729
|
const cache = getHttpCache();
|
|
36586
|
-
const cacheKey = `merged-prs:${owner}/${repo}
|
|
36587
|
-
|
|
36588
|
-
|
|
36589
|
-
|
|
36590
|
-
|
|
36591
|
-
try {
|
|
36592
|
-
const tracker = getSearchBudgetTracker();
|
|
36593
|
-
await tracker.waitForBudget();
|
|
36594
|
-
try {
|
|
36595
|
-
const { data } = await octokit.search.issuesAndPullRequests({
|
|
36596
|
-
q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
|
|
36597
|
-
per_page: 1
|
|
36598
|
-
// We only need total_count
|
|
36599
|
-
});
|
|
36600
|
-
cache.set(cacheKey, "", data.total_count);
|
|
36601
|
-
return data.total_count;
|
|
36602
|
-
} finally {
|
|
36603
|
-
tracker.recordCall();
|
|
36730
|
+
const cacheKey = versionedCacheKey(`merged-prs:${owner}/${repo}`);
|
|
36731
|
+
return withInflightDedup(cache, cacheKey, async () => {
|
|
36732
|
+
const cached2 = cache.getIfFresh(cacheKey, MERGED_PR_CACHE_TTL_MS);
|
|
36733
|
+
if (cached2 != null && typeof cached2 === "number") {
|
|
36734
|
+
return cached2;
|
|
36604
36735
|
}
|
|
36605
|
-
|
|
36606
|
-
|
|
36607
|
-
|
|
36736
|
+
try {
|
|
36737
|
+
const tracker = getSearchBudgetTracker();
|
|
36738
|
+
await tracker.waitForBudget();
|
|
36739
|
+
try {
|
|
36740
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
36741
|
+
q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
|
|
36742
|
+
per_page: 1
|
|
36743
|
+
// We only need total_count
|
|
36744
|
+
});
|
|
36745
|
+
cache.set(cacheKey, "", data.total_count);
|
|
36746
|
+
return data.total_count;
|
|
36747
|
+
} finally {
|
|
36748
|
+
tracker.recordCall();
|
|
36749
|
+
}
|
|
36750
|
+
} catch (error48) {
|
|
36751
|
+
rethrowIfFatal(error48);
|
|
36752
|
+
const errMsg = errorMessage(error48);
|
|
36753
|
+
warn(MODULE5, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Treating as unknown.`);
|
|
36754
|
+
return null;
|
|
36608
36755
|
}
|
|
36609
|
-
|
|
36610
|
-
warn(MODULE5, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
36611
|
-
return 0;
|
|
36612
|
-
}
|
|
36756
|
+
});
|
|
36613
36757
|
}
|
|
36614
36758
|
async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount) {
|
|
36615
36759
|
if (commentCount === 0)
|
|
36616
36760
|
return { passed: true };
|
|
36617
36761
|
try {
|
|
36618
|
-
const
|
|
36619
|
-
|
|
36620
|
-
|
|
36621
|
-
|
|
36622
|
-
|
|
36623
|
-
|
|
36624
|
-
|
|
36762
|
+
const PER_PAGE = 100;
|
|
36763
|
+
const lastPage = Math.max(1, Math.ceil(commentCount / PER_PAGE));
|
|
36764
|
+
const pagesToFetch = lastPage > 1 ? [lastPage - 1, lastPage] : [1];
|
|
36765
|
+
const recentComments = [];
|
|
36766
|
+
for (const page of pagesToFetch) {
|
|
36767
|
+
const response = await octokit.issues.listComments({
|
|
36768
|
+
owner,
|
|
36769
|
+
repo,
|
|
36770
|
+
issue_number: issueNumber,
|
|
36771
|
+
per_page: PER_PAGE,
|
|
36772
|
+
page
|
|
36773
|
+
});
|
|
36774
|
+
recentComments.push(...response.data);
|
|
36775
|
+
}
|
|
36625
36776
|
for (const comment of recentComments) {
|
|
36626
|
-
|
|
36627
|
-
if (CLAIM_PHRASES.some((phrase) => body.includes(phrase))) {
|
|
36777
|
+
if (commentClaimsIssue(comment.body || "")) {
|
|
36628
36778
|
return { passed: false };
|
|
36629
36779
|
}
|
|
36630
36780
|
}
|
|
36631
36781
|
return { passed: true };
|
|
36632
36782
|
} catch (error48) {
|
|
36633
|
-
|
|
36634
|
-
throw error48;
|
|
36635
|
-
}
|
|
36783
|
+
rethrowIfFatal(error48);
|
|
36636
36784
|
const errMsg = errorMessage(error48);
|
|
36637
36785
|
warn(MODULE5, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
36638
36786
|
return { passed: true, inconclusive: true, reason: errMsg };
|
|
@@ -36705,19 +36853,11 @@ async function checkProjectHealth(octokit, owner, repo) {
|
|
|
36705
36853
|
};
|
|
36706
36854
|
});
|
|
36707
36855
|
} catch (error48) {
|
|
36708
|
-
|
|
36709
|
-
throw error48;
|
|
36710
|
-
}
|
|
36856
|
+
rethrowIfFatal(error48);
|
|
36711
36857
|
const errMsg = errorMessage(error48);
|
|
36712
36858
|
warn(MODULE6, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
36713
36859
|
return {
|
|
36714
36860
|
repo: `${owner}/${repo}`,
|
|
36715
|
-
lastCommitAt: "",
|
|
36716
|
-
daysSinceLastCommit: 999,
|
|
36717
|
-
openIssuesCount: 0,
|
|
36718
|
-
avgIssueResponseDays: 0,
|
|
36719
|
-
ciStatus: "unknown",
|
|
36720
|
-
isActive: false,
|
|
36721
36861
|
checkFailed: true,
|
|
36722
36862
|
failureReason: errMsg
|
|
36723
36863
|
};
|
|
@@ -36729,6 +36869,20 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
36729
36869
|
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
|
|
36730
36870
|
return cached2.guidelines;
|
|
36731
36871
|
}
|
|
36872
|
+
const inflight = guidelinesInflight.get(cacheKey);
|
|
36873
|
+
if (inflight)
|
|
36874
|
+
return inflight;
|
|
36875
|
+
const promise2 = fetchContributionGuidelinesUncached(octokit, owner, repo);
|
|
36876
|
+
guidelinesInflight.set(cacheKey, promise2);
|
|
36877
|
+
try {
|
|
36878
|
+
return await promise2;
|
|
36879
|
+
} finally {
|
|
36880
|
+
guidelinesInflight.delete(cacheKey);
|
|
36881
|
+
}
|
|
36882
|
+
}
|
|
36883
|
+
var guidelinesInflight = /* @__PURE__ */ new Map();
|
|
36884
|
+
async function fetchContributionGuidelinesUncached(octokit, owner, repo) {
|
|
36885
|
+
const cacheKey = `${owner}/${repo}`;
|
|
36732
36886
|
const filesToCheck = [
|
|
36733
36887
|
"CONTRIBUTING.md",
|
|
36734
36888
|
".github/CONTRIBUTING.md",
|
|
@@ -36776,7 +36930,7 @@ function parseContributionGuidelines(content) {
|
|
|
36776
36930
|
};
|
|
36777
36931
|
const lowerContent = content.toLowerCase();
|
|
36778
36932
|
if (lowerContent.includes("branch")) {
|
|
36779
|
-
const branchMatch = content.match(/branch[^\n]
|
|
36933
|
+
const branchMatch = content.match(/branch[^\n]{0,200}?(?:named?|format|convention)[^\n]{0,200}?[`"]([^`"\n]{1,100})[`"]/i);
|
|
36780
36934
|
if (branchMatch) {
|
|
36781
36935
|
guidelines.branchNamingConvention = branchMatch[1];
|
|
36782
36936
|
}
|
|
@@ -36784,7 +36938,7 @@ function parseContributionGuidelines(content) {
|
|
|
36784
36938
|
if (lowerContent.includes("conventional commit")) {
|
|
36785
36939
|
guidelines.commitMessageFormat = "conventional commits";
|
|
36786
36940
|
} else if (lowerContent.includes("commit message")) {
|
|
36787
|
-
const commitMatch = content.match(/commit message[^\n]
|
|
36941
|
+
const commitMatch = content.match(/commit message[^\n]{0,200}?[`"]([^`"\n]{1,100})[`"]/i);
|
|
36788
36942
|
if (commitMatch) {
|
|
36789
36943
|
guidelines.commitMessageFormat = commitMatch[1];
|
|
36790
36944
|
}
|
|
@@ -36803,7 +36957,7 @@ function parseContributionGuidelines(content) {
|
|
|
36803
36957
|
guidelines.linter = "RuboCop";
|
|
36804
36958
|
else if (lowerContent.includes("prettier"))
|
|
36805
36959
|
guidelines.formatter = "Prettier";
|
|
36806
|
-
if (lowerContent
|
|
36960
|
+
if (/\bcla\b/.test(lowerContent) || lowerContent.includes("contributor license agreement")) {
|
|
36807
36961
|
guidelines.claRequired = true;
|
|
36808
36962
|
}
|
|
36809
36963
|
return guidelines;
|
|
@@ -36887,8 +37041,7 @@ async function fetchFileText(octokit, owner, repo, path5) {
|
|
|
36887
37041
|
const status = getHttpStatusCode(error48);
|
|
36888
37042
|
if (status === 404)
|
|
36889
37043
|
return { text: null, transient: false };
|
|
36890
|
-
|
|
36891
|
-
throw error48;
|
|
37044
|
+
rethrowIfFatal(error48);
|
|
36892
37045
|
warn(MODULE7, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
|
|
36893
37046
|
return { text: null, transient: true };
|
|
36894
37047
|
}
|
|
@@ -36925,7 +37078,7 @@ function isAntiLLMPolicyResult(value) {
|
|
|
36925
37078
|
}
|
|
36926
37079
|
async function fetchAndScanAntiLLMPolicy(octokit, owner, repo, options) {
|
|
36927
37080
|
const cache = getHttpCache();
|
|
36928
|
-
const cacheKey = `anti-llm-policy:${owner}/${repo}
|
|
37081
|
+
const cacheKey = versionedCacheKey(`anti-llm-policy:${owner}/${repo}`);
|
|
36929
37082
|
const cached2 = cache.getIfFresh(cacheKey, POLICY_SCAN_CACHE_TTL_MS);
|
|
36930
37083
|
if (isAntiLLMPolicyResult(cached2))
|
|
36931
37084
|
return cached2;
|
|
@@ -36964,6 +37117,76 @@ async function fetchAndScanAntiLLMPolicy(octokit, owner, repo, options) {
|
|
|
36964
37117
|
return noMatch;
|
|
36965
37118
|
}
|
|
36966
37119
|
|
|
37120
|
+
// ../core/dist/core/issue-graphql.js
|
|
37121
|
+
var MODULE8 = "issue-graphql";
|
|
37122
|
+
function issueCoreKey(owner, repo, number4) {
|
|
37123
|
+
return `${owner}/${repo}#${number4}`;
|
|
37124
|
+
}
|
|
37125
|
+
function normalizeNode(node) {
|
|
37126
|
+
return {
|
|
37127
|
+
id: node.databaseId,
|
|
37128
|
+
title: node.title,
|
|
37129
|
+
body: node.body ?? "",
|
|
37130
|
+
state: node.state === "CLOSED" ? "closed" : "open",
|
|
37131
|
+
labels: node.labels.nodes.map((l) => l.name),
|
|
37132
|
+
commentCount: node.comments.totalCount,
|
|
37133
|
+
createdAt: node.createdAt,
|
|
37134
|
+
updatedAt: node.updatedAt
|
|
37135
|
+
};
|
|
37136
|
+
}
|
|
37137
|
+
async function prefetchIssueCores(octokit, issues) {
|
|
37138
|
+
const result = /* @__PURE__ */ new Map();
|
|
37139
|
+
if (issues.length === 0)
|
|
37140
|
+
return result;
|
|
37141
|
+
const unique = [
|
|
37142
|
+
...new Map(issues.map((i) => [issueCoreKey(i.owner, i.repo, i.number), i])).values()
|
|
37143
|
+
];
|
|
37144
|
+
const varDefs = [];
|
|
37145
|
+
const selections = [];
|
|
37146
|
+
const variables = {};
|
|
37147
|
+
unique.forEach((iss, i) => {
|
|
37148
|
+
varDefs.push(`$o${i}: String!, $n${i}: String!, $num${i}: Int!`);
|
|
37149
|
+
variables[`o${i}`] = iss.owner;
|
|
37150
|
+
variables[`n${i}`] = iss.repo;
|
|
37151
|
+
variables[`num${i}`] = iss.number;
|
|
37152
|
+
selections.push(`i${i}: repository(owner: $o${i}, name: $n${i}) {
|
|
37153
|
+
issue(number: $num${i}) {
|
|
37154
|
+
databaseId
|
|
37155
|
+
title
|
|
37156
|
+
body
|
|
37157
|
+
state
|
|
37158
|
+
labels(first: 100) { nodes { name } }
|
|
37159
|
+
comments { totalCount }
|
|
37160
|
+
createdAt
|
|
37161
|
+
updatedAt
|
|
37162
|
+
}
|
|
37163
|
+
}`);
|
|
37164
|
+
});
|
|
37165
|
+
const query = `query batchIssueCores(${varDefs.join(", ")}) {
|
|
37166
|
+
${selections.join("\n")}
|
|
37167
|
+
}`;
|
|
37168
|
+
let data;
|
|
37169
|
+
try {
|
|
37170
|
+
data = await octokit.graphql(query, variables);
|
|
37171
|
+
} catch (err) {
|
|
37172
|
+
rethrowIfFatal(err);
|
|
37173
|
+
const partial2 = err.data;
|
|
37174
|
+
if (partial2) {
|
|
37175
|
+
data = partial2;
|
|
37176
|
+
} else {
|
|
37177
|
+
warn(MODULE8, `GraphQL prefetch failed, falling back to REST: ${errorMessage(err)}`);
|
|
37178
|
+
return result;
|
|
37179
|
+
}
|
|
37180
|
+
}
|
|
37181
|
+
unique.forEach((iss, i) => {
|
|
37182
|
+
const node = data?.[`i${i}`]?.issue;
|
|
37183
|
+
if (!node || node.databaseId == null)
|
|
37184
|
+
return;
|
|
37185
|
+
result.set(issueCoreKey(iss.owner, iss.repo, iss.number), normalizeNode(node));
|
|
37186
|
+
});
|
|
37187
|
+
return result;
|
|
37188
|
+
}
|
|
37189
|
+
|
|
36967
37190
|
// ../core/dist/core/slm-triage.js
|
|
36968
37191
|
var DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
|
|
36969
37192
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -37076,9 +37299,81 @@ function isValidTriageShape(value) {
|
|
|
37076
37299
|
}
|
|
37077
37300
|
|
|
37078
37301
|
// ../core/dist/core/issue-vetting.js
|
|
37079
|
-
var
|
|
37302
|
+
var MODULE9 = "issue-vetting";
|
|
37080
37303
|
var MAX_CONCURRENT_VETTING = 3;
|
|
37081
37304
|
var VETTING_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
37305
|
+
function deriveRecommendation(input) {
|
|
37306
|
+
const notes = [];
|
|
37307
|
+
const reasonsToApprove = [];
|
|
37308
|
+
const reasonsToSkip = [];
|
|
37309
|
+
if (!input.noExistingPR)
|
|
37310
|
+
notes.push(input.ownPR ? "Your PR is already in flight for this issue" : "Existing PR found for this issue");
|
|
37311
|
+
if (!input.notClaimed)
|
|
37312
|
+
notes.push("Issue appears to be claimed by someone");
|
|
37313
|
+
if (input.existingPRInconclusive) {
|
|
37314
|
+
notes.push(`Could not verify absence of existing PRs: ${input.existingPRReason || "API error"}`);
|
|
37315
|
+
}
|
|
37316
|
+
if (input.claimInconclusive) {
|
|
37317
|
+
notes.push(`Could not verify claim status: ${input.claimReason || "API error"}`);
|
|
37318
|
+
}
|
|
37319
|
+
if (input.projectCheckFailed) {
|
|
37320
|
+
notes.push(`Could not verify project activity: ${input.projectFailureReason || "API error"}`);
|
|
37321
|
+
} else if (!input.projectIsActive) {
|
|
37322
|
+
notes.push("Project may be inactive");
|
|
37323
|
+
}
|
|
37324
|
+
if (!input.clearRequirements)
|
|
37325
|
+
notes.push("Issue requirements are unclear");
|
|
37326
|
+
if (!input.contributionGuidelinesFound)
|
|
37327
|
+
notes.push("No CONTRIBUTING.md found");
|
|
37328
|
+
if (!input.noExistingPR)
|
|
37329
|
+
reasonsToSkip.push(input.ownPR ? "You already have a PR in flight" : "Has existing PR");
|
|
37330
|
+
if (!input.notClaimed)
|
|
37331
|
+
reasonsToSkip.push("Already claimed");
|
|
37332
|
+
if (!input.projectIsActive && !input.projectCheckFailed)
|
|
37333
|
+
reasonsToSkip.push("Inactive project");
|
|
37334
|
+
if (!input.clearRequirements)
|
|
37335
|
+
reasonsToSkip.push("Unclear requirements");
|
|
37336
|
+
if (input.noExistingPR)
|
|
37337
|
+
reasonsToApprove.push("No existing PR");
|
|
37338
|
+
if (input.notClaimed)
|
|
37339
|
+
reasonsToApprove.push("Not claimed");
|
|
37340
|
+
if (input.projectIsActive && !input.projectCheckFailed)
|
|
37341
|
+
reasonsToApprove.push("Active project");
|
|
37342
|
+
if (input.clearRequirements)
|
|
37343
|
+
reasonsToApprove.push("Clear requirements");
|
|
37344
|
+
if (input.contributionGuidelinesFound)
|
|
37345
|
+
reasonsToApprove.push("Has contribution guidelines");
|
|
37346
|
+
if (input.effectiveMergedCount > 0) {
|
|
37347
|
+
reasonsToApprove.push(`Trusted project (${input.effectiveMergedCount} PR${input.effectiveMergedCount > 1 ? "s" : ""} merged)`);
|
|
37348
|
+
}
|
|
37349
|
+
if (input.orgHasMergedPRs) {
|
|
37350
|
+
reasonsToApprove.push(`Org affinity (merged PRs in other ${input.orgName} repos)`);
|
|
37351
|
+
}
|
|
37352
|
+
if (input.matchesCategory) {
|
|
37353
|
+
reasonsToApprove.push("Matches preferred project category");
|
|
37354
|
+
}
|
|
37355
|
+
if (input.issueClosed) {
|
|
37356
|
+
reasonsToSkip.push("Issue is closed");
|
|
37357
|
+
}
|
|
37358
|
+
let recommendation;
|
|
37359
|
+
if (input.issueClosed) {
|
|
37360
|
+
recommendation = "skip";
|
|
37361
|
+
} else if (input.ownPR) {
|
|
37362
|
+
recommendation = "skip";
|
|
37363
|
+
} else if (input.passedAllChecks) {
|
|
37364
|
+
recommendation = "approve";
|
|
37365
|
+
} else if (reasonsToSkip.length > 2) {
|
|
37366
|
+
recommendation = "skip";
|
|
37367
|
+
} else {
|
|
37368
|
+
recommendation = "needs_review";
|
|
37369
|
+
}
|
|
37370
|
+
const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive;
|
|
37371
|
+
if (recommendation === "approve" && hasInconclusiveChecks) {
|
|
37372
|
+
recommendation = "needs_review";
|
|
37373
|
+
notes.push("Recommendation downgraded: one or more checks were inconclusive");
|
|
37374
|
+
}
|
|
37375
|
+
return { notes, reasonsToApprove, reasonsToSkip, recommendation };
|
|
37376
|
+
}
|
|
37082
37377
|
var IssueVetter = class {
|
|
37083
37378
|
octokit;
|
|
37084
37379
|
stateReader;
|
|
@@ -37098,10 +37393,10 @@ var IssueVetter = class {
|
|
|
37098
37393
|
async vetIssue(issueUrl, opts) {
|
|
37099
37394
|
const cache = getHttpCache();
|
|
37100
37395
|
const sigKey = opts?.featureSignals ? `:r${opts.featureSignals.reactions}c${opts.featureSignals.comments}m${opts.featureSignals.hasMilestone ? 1 : 0}o${opts.featureSignals.onRoadmap ? 1 : 0}w${opts.featureSignals.wontfixNoContributor ? 1 : 0}` : "";
|
|
37101
|
-
const cacheKey = `vet:${issueUrl}${sigKey}
|
|
37396
|
+
const cacheKey = versionedCacheKey(`vet:${issueUrl}${sigKey}`);
|
|
37102
37397
|
const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
|
|
37103
37398
|
if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
|
|
37104
|
-
debug(
|
|
37399
|
+
debug(MODULE9, `Vetting cache hit for ${issueUrl}`);
|
|
37105
37400
|
return cached2;
|
|
37106
37401
|
}
|
|
37107
37402
|
const parsed = parseGitHubUrl(issueUrl);
|
|
@@ -37110,16 +37405,12 @@ var IssueVetter = class {
|
|
|
37110
37405
|
}
|
|
37111
37406
|
const { owner, repo, number: number4 } = parsed;
|
|
37112
37407
|
const repoFullName = `${owner}/${repo}`;
|
|
37113
|
-
const
|
|
37114
|
-
owner,
|
|
37115
|
-
repo,
|
|
37116
|
-
issue_number: number4
|
|
37117
|
-
});
|
|
37408
|
+
const core = opts?.prefetched ?? await this.fetchIssueCore(owner, repo, number4);
|
|
37118
37409
|
const reposWithMergedPRs = this.stateReader.getReposWithMergedPRs();
|
|
37119
37410
|
const hasMergedPRsInRepo = reposWithMergedPRs.includes(repoFullName);
|
|
37120
37411
|
const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
|
|
37121
37412
|
checkNoExistingPR(this.octokit, owner, repo, number4),
|
|
37122
|
-
checkNotClaimed(this.octokit, owner, repo, number4,
|
|
37413
|
+
checkNotClaimed(this.octokit, owner, repo, number4, core.commentCount),
|
|
37123
37414
|
checkProjectHealth(this.octokit, owner, repo),
|
|
37124
37415
|
fetchContributionGuidelines(this.octokit, owner, repo),
|
|
37125
37416
|
hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo)
|
|
@@ -37127,7 +37418,10 @@ var IssueVetter = class {
|
|
|
37127
37418
|
const antiLLMPolicy = await fetchAndScanAntiLLMPolicy(this.octokit, owner, repo, { contributingText: contributionGuidelines?.rawContent });
|
|
37128
37419
|
const noExistingPR = existingPRCheck.passed;
|
|
37129
37420
|
const notClaimed = claimCheck.passed;
|
|
37130
|
-
const
|
|
37421
|
+
const username = this.stateReader.getGitHubUsername?.() ?? "";
|
|
37422
|
+
const linkedPR = existingPRCheck.linkedPR;
|
|
37423
|
+
const ownPR = !noExistingPR && !!linkedPR && linkedPR.state === "open" && !linkedPR.merged && username !== "" && linkedPR.author.toLowerCase() === username.toLowerCase();
|
|
37424
|
+
const clearRequirements = analyzeRequirements(core.body);
|
|
37131
37425
|
const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
|
|
37132
37426
|
const vettingResult = {
|
|
37133
37427
|
passedAllChecks: noExistingPR && notClaimed && projectActive && clearRequirements,
|
|
@@ -37142,89 +37436,52 @@ var IssueVetter = class {
|
|
|
37142
37436
|
linkedPR: existingPRCheck.linkedPR,
|
|
37143
37437
|
notes: []
|
|
37144
37438
|
};
|
|
37145
|
-
if (!noExistingPR)
|
|
37146
|
-
vettingResult.notes.push("Existing PR found for this issue");
|
|
37147
|
-
if (!notClaimed)
|
|
37148
|
-
vettingResult.notes.push("Issue appears to be claimed by someone");
|
|
37149
|
-
if (existingPRCheck.inconclusive) {
|
|
37150
|
-
vettingResult.notes.push(`Could not verify absence of existing PRs: ${existingPRCheck.reason || "API error"}`);
|
|
37151
|
-
}
|
|
37152
|
-
if (claimCheck.inconclusive) {
|
|
37153
|
-
vettingResult.notes.push(`Could not verify claim status: ${claimCheck.reason || "API error"}`);
|
|
37154
|
-
}
|
|
37155
|
-
if (projectHealth.checkFailed) {
|
|
37156
|
-
vettingResult.notes.push(`Could not verify project activity: ${projectHealth.failureReason || "API error"}`);
|
|
37157
|
-
} else if (!projectHealth.isActive) {
|
|
37158
|
-
vettingResult.notes.push("Project may be inactive");
|
|
37159
|
-
}
|
|
37160
|
-
if (!clearRequirements)
|
|
37161
|
-
vettingResult.notes.push("Issue requirements are unclear");
|
|
37162
|
-
if (!contributionGuidelines)
|
|
37163
|
-
vettingResult.notes.push("No CONTRIBUTING.md found");
|
|
37164
37439
|
const trackedIssue = {
|
|
37165
|
-
id:
|
|
37440
|
+
id: core.id,
|
|
37166
37441
|
url: issueUrl,
|
|
37167
37442
|
repo: repoFullName,
|
|
37168
37443
|
number: number4,
|
|
37169
|
-
title:
|
|
37444
|
+
title: core.title,
|
|
37170
37445
|
status: "candidate",
|
|
37171
|
-
labels:
|
|
37172
|
-
createdAt:
|
|
37173
|
-
updatedAt:
|
|
37446
|
+
labels: core.labels,
|
|
37447
|
+
createdAt: core.createdAt,
|
|
37448
|
+
updatedAt: core.updatedAt,
|
|
37174
37449
|
vetted: true,
|
|
37175
37450
|
vettingResult
|
|
37176
37451
|
};
|
|
37177
|
-
const
|
|
37178
|
-
const
|
|
37179
|
-
if (!noExistingPR)
|
|
37180
|
-
reasonsToSkip.push("Has existing PR");
|
|
37181
|
-
if (!notClaimed)
|
|
37182
|
-
reasonsToSkip.push("Already claimed");
|
|
37183
|
-
if (!projectHealth.isActive && !projectHealth.checkFailed)
|
|
37184
|
-
reasonsToSkip.push("Inactive project");
|
|
37185
|
-
if (!clearRequirements)
|
|
37186
|
-
reasonsToSkip.push("Unclear requirements");
|
|
37187
|
-
if (noExistingPR)
|
|
37188
|
-
reasonsToApprove.push("No existing PR");
|
|
37189
|
-
if (notClaimed)
|
|
37190
|
-
reasonsToApprove.push("Not claimed");
|
|
37191
|
-
if (projectHealth.isActive && !projectHealth.checkFailed)
|
|
37192
|
-
reasonsToApprove.push("Active project");
|
|
37193
|
-
if (clearRequirements)
|
|
37194
|
-
reasonsToApprove.push("Clear requirements");
|
|
37195
|
-
if (contributionGuidelines)
|
|
37196
|
-
reasonsToApprove.push("Has contribution guidelines");
|
|
37197
|
-
const effectiveMergedCount = hasMergedPRsInRepo ? 1 : userMergedPRCount;
|
|
37198
|
-
if (effectiveMergedCount > 0) {
|
|
37199
|
-
reasonsToApprove.push(`Trusted project (${effectiveMergedCount} PR${effectiveMergedCount > 1 ? "s" : ""} merged)`);
|
|
37200
|
-
}
|
|
37452
|
+
const mergedCountInconclusive = !hasMergedPRsInRepo && userMergedPRCount === null;
|
|
37453
|
+
const effectiveMergedCount = hasMergedPRsInRepo ? 1 : userMergedPRCount ?? 0;
|
|
37201
37454
|
const orgName = repoFullName.split("/")[0];
|
|
37202
37455
|
let orgHasMergedPRs = false;
|
|
37203
37456
|
if (orgName && repoFullName.includes("/")) {
|
|
37204
37457
|
orgHasMergedPRs = reposWithMergedPRs.some((r) => r.startsWith(orgName + "/") && r !== repoFullName);
|
|
37205
37458
|
}
|
|
37206
|
-
|
|
37207
|
-
|
|
37208
|
-
|
|
37209
|
-
const
|
|
37210
|
-
|
|
37211
|
-
|
|
37212
|
-
|
|
37213
|
-
|
|
37214
|
-
|
|
37215
|
-
|
|
37216
|
-
|
|
37217
|
-
|
|
37218
|
-
|
|
37219
|
-
|
|
37220
|
-
|
|
37221
|
-
|
|
37222
|
-
|
|
37223
|
-
|
|
37224
|
-
|
|
37225
|
-
|
|
37226
|
-
|
|
37227
|
-
|
|
37459
|
+
const matchesCategory = repoBelongsToCategory(repoFullName, this.stateReader.getProjectCategories());
|
|
37460
|
+
const issueClosed = core.state === "closed";
|
|
37461
|
+
const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive;
|
|
37462
|
+
const { notes, reasonsToApprove, reasonsToSkip, recommendation } = deriveRecommendation({
|
|
37463
|
+
noExistingPR,
|
|
37464
|
+
ownPR,
|
|
37465
|
+
notClaimed,
|
|
37466
|
+
clearRequirements,
|
|
37467
|
+
contributionGuidelinesFound: !!contributionGuidelines,
|
|
37468
|
+
projectIsActive: projectHealth.checkFailed ? false : projectHealth.isActive,
|
|
37469
|
+
projectCheckFailed: !!projectHealth.checkFailed,
|
|
37470
|
+
projectFailureReason: projectHealth.failureReason,
|
|
37471
|
+
existingPRInconclusive: !!existingPRCheck.inconclusive,
|
|
37472
|
+
existingPRReason: existingPRCheck.inconclusive ? existingPRCheck.reason : void 0,
|
|
37473
|
+
claimInconclusive: !!claimCheck.inconclusive,
|
|
37474
|
+
claimReason: claimCheck.inconclusive ? claimCheck.reason : void 0,
|
|
37475
|
+
mergedCountInconclusive,
|
|
37476
|
+
effectiveMergedCount,
|
|
37477
|
+
orgName,
|
|
37478
|
+
orgHasMergedPRs,
|
|
37479
|
+
matchesCategory,
|
|
37480
|
+
issueClosed,
|
|
37481
|
+
passedAllChecks: vettingResult.passedAllChecks
|
|
37482
|
+
});
|
|
37483
|
+
vettingResult.notes = notes;
|
|
37484
|
+
const repoQualityBonus = projectHealth.checkFailed ? 0 : calculateRepoQualityBonus(projectHealth.stargazersCount ?? 0, projectHealth.forksCount ?? 0);
|
|
37228
37485
|
if (projectHealth.checkFailed && repoQualityBonus === 0) {
|
|
37229
37486
|
vettingResult.notes.push("Repo quality bonus unavailable: could not fetch star/fork counts due to API error");
|
|
37230
37487
|
}
|
|
@@ -37235,8 +37492,8 @@ var IssueVetter = class {
|
|
|
37235
37492
|
isClaimed: !notClaimed,
|
|
37236
37493
|
clearRequirements,
|
|
37237
37494
|
hasContributionGuidelines: !!contributionGuidelines,
|
|
37238
|
-
issueUpdatedAt:
|
|
37239
|
-
closedWithoutMergeCount: 0,
|
|
37495
|
+
issueUpdatedAt: core.updatedAt,
|
|
37496
|
+
closedWithoutMergeCount: this.stateReader.getClosedWithoutMergeCount?.(repoFullName) ?? 0,
|
|
37240
37497
|
mergedPRCount: effectiveMergedCount,
|
|
37241
37498
|
orgHasMergedPRs,
|
|
37242
37499
|
repoQualityBonus,
|
|
@@ -37250,22 +37507,20 @@ var IssueVetter = class {
|
|
|
37250
37507
|
} else if (starredRepos.includes(repoFullName)) {
|
|
37251
37508
|
searchPriority = "starred";
|
|
37252
37509
|
}
|
|
37253
|
-
const slmConfig = this.stateReader.getSLMTriageConfig
|
|
37254
|
-
model: "",
|
|
37255
|
-
host: ""
|
|
37256
|
-
};
|
|
37510
|
+
const slmConfig = this.stateReader.getSLMTriageConfig();
|
|
37257
37511
|
let slmTriage = null;
|
|
37258
|
-
if (slmConfig
|
|
37512
|
+
if (slmConfig) {
|
|
37259
37513
|
const slmOpts = { model: slmConfig.model };
|
|
37260
37514
|
if (slmConfig.host)
|
|
37261
37515
|
slmOpts.host = slmConfig.host;
|
|
37262
37516
|
slmTriage = await triageWithSLM(buildTriageInput({
|
|
37263
|
-
issue: { ...trackedIssue, body:
|
|
37517
|
+
issue: { ...trackedIssue, body: core.body },
|
|
37264
37518
|
linkedPR: existingPRCheck.linkedPR ?? null
|
|
37265
37519
|
}), slmOpts);
|
|
37266
37520
|
}
|
|
37267
37521
|
const result = {
|
|
37268
37522
|
issue: trackedIssue,
|
|
37523
|
+
issueState: issueClosed ? "closed" : "open",
|
|
37269
37524
|
vettingResult,
|
|
37270
37525
|
projectHealth,
|
|
37271
37526
|
antiLLMPolicy,
|
|
@@ -37276,9 +37531,33 @@ var IssueVetter = class {
|
|
|
37276
37531
|
viabilityScore,
|
|
37277
37532
|
searchPriority
|
|
37278
37533
|
};
|
|
37279
|
-
|
|
37534
|
+
if (!hasInconclusiveChecks) {
|
|
37535
|
+
cache.set(cacheKey, "", result);
|
|
37536
|
+
}
|
|
37280
37537
|
return result;
|
|
37281
37538
|
}
|
|
37539
|
+
/**
|
|
37540
|
+
* Fetch a single issue's core fields via REST and normalize them to the same
|
|
37541
|
+
* shape as a GraphQL prefetch (#169). The REST fallback path when no
|
|
37542
|
+
* prefetched core was supplied.
|
|
37543
|
+
*/
|
|
37544
|
+
async fetchIssueCore(owner, repo, number4) {
|
|
37545
|
+
const { data: ghIssue } = await this.octokit.issues.get({
|
|
37546
|
+
owner,
|
|
37547
|
+
repo,
|
|
37548
|
+
issue_number: number4
|
|
37549
|
+
});
|
|
37550
|
+
return {
|
|
37551
|
+
id: ghIssue.id,
|
|
37552
|
+
title: ghIssue.title,
|
|
37553
|
+
body: ghIssue.body || "",
|
|
37554
|
+
state: ghIssue.state === "closed" ? "closed" : "open",
|
|
37555
|
+
labels: ghIssue.labels.map((l) => typeof l === "string" ? l : l.name || ""),
|
|
37556
|
+
commentCount: ghIssue.comments,
|
|
37557
|
+
createdAt: ghIssue.created_at,
|
|
37558
|
+
updatedAt: ghIssue.updated_at
|
|
37559
|
+
};
|
|
37560
|
+
}
|
|
37282
37561
|
/**
|
|
37283
37562
|
* Vet multiple issues in parallel with concurrency limit
|
|
37284
37563
|
*/
|
|
@@ -37289,13 +37568,23 @@ var IssueVetter = class {
|
|
|
37289
37568
|
let rateLimitFailures = 0;
|
|
37290
37569
|
let attemptedCount = 0;
|
|
37291
37570
|
let firstAuthError = null;
|
|
37292
|
-
|
|
37571
|
+
const uniqueUrls = [...new Set(urls)];
|
|
37572
|
+
const prefetched = await prefetchIssueCores(this.octokit, uniqueUrls.flatMap((url2) => {
|
|
37573
|
+
const p = parseGitHubUrl(url2);
|
|
37574
|
+
return p && p.type === "issues" ? [{ owner: p.owner, repo: p.repo, number: p.number }] : [];
|
|
37575
|
+
}));
|
|
37576
|
+
const prefetchFor = (url2) => {
|
|
37577
|
+
const p = parseGitHubUrl(url2);
|
|
37578
|
+
return p && p.type === "issues" ? prefetched.get(issueCoreKey(p.owner, p.repo, p.number)) : void 0;
|
|
37579
|
+
};
|
|
37580
|
+
for (const url2 of uniqueUrls) {
|
|
37293
37581
|
if (candidates.length >= maxResults)
|
|
37294
37582
|
break;
|
|
37295
37583
|
if (firstAuthError)
|
|
37296
37584
|
break;
|
|
37297
37585
|
attemptedCount++;
|
|
37298
|
-
const
|
|
37586
|
+
const core = prefetchFor(url2);
|
|
37587
|
+
const task = this.vetIssue(url2, core ? { prefetched: core } : void 0).then((candidate) => {
|
|
37299
37588
|
if (candidates.length < maxResults) {
|
|
37300
37589
|
if (priority) {
|
|
37301
37590
|
candidate.searchPriority = priority;
|
|
@@ -37311,7 +37600,7 @@ var IssueVetter = class {
|
|
|
37311
37600
|
if (isRateLimitError(error48)) {
|
|
37312
37601
|
rateLimitFailures++;
|
|
37313
37602
|
}
|
|
37314
|
-
warn(
|
|
37603
|
+
warn(MODULE9, `Error vetting issue ${url2}:`, errorMessage(error48));
|
|
37315
37604
|
}).finally(() => pending.delete(url2));
|
|
37316
37605
|
pending.set(url2, task);
|
|
37317
37606
|
if (pending.size >= MAX_CONCURRENT_VETTING) {
|
|
@@ -37321,13 +37610,13 @@ var IssueVetter = class {
|
|
|
37321
37610
|
await Promise.allSettled(pending.values());
|
|
37322
37611
|
if (firstAuthError) {
|
|
37323
37612
|
if (candidates.length > 0) {
|
|
37324
|
-
warn(
|
|
37613
|
+
warn(MODULE9, `Auth failed mid-batch after ${candidates.length} successful vet(s) \u2014 discarding partial results`);
|
|
37325
37614
|
}
|
|
37326
37615
|
throw firstAuthError;
|
|
37327
37616
|
}
|
|
37328
37617
|
const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
|
|
37329
37618
|
if (allFailed) {
|
|
37330
|
-
warn(
|
|
37619
|
+
warn(MODULE9, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
37331
37620
|
}
|
|
37332
37621
|
return {
|
|
37333
37622
|
candidates: candidates.slice(0, maxResults),
|
|
@@ -37338,14 +37627,14 @@ var IssueVetter = class {
|
|
|
37338
37627
|
};
|
|
37339
37628
|
|
|
37340
37629
|
// ../core/dist/core/search-phases.js
|
|
37341
|
-
var
|
|
37630
|
+
var MODULE10 = "search-phases";
|
|
37342
37631
|
var GITHUB_MAX_BOOLEAN_OPS = 5;
|
|
37343
37632
|
var INTER_QUERY_DELAY_MS = 2e3;
|
|
37344
37633
|
function chunkLabels(labels, reservedOps = 0) {
|
|
37345
37634
|
const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
|
|
37346
37635
|
if (maxPerChunk < 1) {
|
|
37347
37636
|
if (labels.length > 0) {
|
|
37348
|
-
warn(
|
|
37637
|
+
warn(MODULE10, `Label filtering disabled: ${reservedOps} repo/org ORs exceed GitHub's ${GITHUB_MAX_BOOLEAN_OPS} operator limit. All ${labels.length} label(s) dropped from query.`);
|
|
37349
37638
|
}
|
|
37350
37639
|
return [[]];
|
|
37351
37640
|
}
|
|
@@ -37355,7 +37644,7 @@ function chunkLabels(labels, reservedOps = 0) {
|
|
|
37355
37644
|
for (let i = 0; i < labels.length; i += maxPerChunk) {
|
|
37356
37645
|
chunks.push(labels.slice(i, i + maxPerChunk));
|
|
37357
37646
|
}
|
|
37358
|
-
debug(
|
|
37647
|
+
debug(MODULE10, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
|
|
37359
37648
|
return chunks;
|
|
37360
37649
|
}
|
|
37361
37650
|
function buildLabelQuery(labels) {
|
|
@@ -37388,11 +37677,11 @@ function interleaveArrays(arrays) {
|
|
|
37388
37677
|
}
|
|
37389
37678
|
var SEARCH_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
37390
37679
|
async function cachedSearchIssues(octokit, params) {
|
|
37391
|
-
const cacheKey = `search:${params.q}:${params.sort}:${params.order}:${params.per_page}
|
|
37680
|
+
const cacheKey = versionedCacheKey(`search:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
|
|
37392
37681
|
const cache = getHttpCache();
|
|
37393
37682
|
const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
|
|
37394
37683
|
if (cached2) {
|
|
37395
|
-
debug(
|
|
37684
|
+
debug(MODULE10, `Search cache hit for query`);
|
|
37396
37685
|
return cached2;
|
|
37397
37686
|
}
|
|
37398
37687
|
const tracker = getSearchBudgetTracker();
|
|
@@ -37407,7 +37696,7 @@ async function cachedSearchIssues(octokit, params) {
|
|
|
37407
37696
|
if (data.items.length > 0) {
|
|
37408
37697
|
cache.set(cacheKey, "", data);
|
|
37409
37698
|
} else {
|
|
37410
|
-
debug(
|
|
37699
|
+
debug(MODULE10, `Skipping cache for empty search result (possible rate limit artifact)`);
|
|
37411
37700
|
}
|
|
37412
37701
|
return data;
|
|
37413
37702
|
}
|
|
@@ -37455,10 +37744,10 @@ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResul
|
|
|
37455
37744
|
if (getHttpStatusCode(error48) === 401)
|
|
37456
37745
|
throw error48;
|
|
37457
37746
|
if (isRateLimitError(error48)) {
|
|
37458
|
-
warn(
|
|
37747
|
+
warn(MODULE10, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37459
37748
|
break;
|
|
37460
37749
|
}
|
|
37461
|
-
warn(
|
|
37750
|
+
warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37462
37751
|
}
|
|
37463
37752
|
}
|
|
37464
37753
|
return items;
|
|
@@ -37475,16 +37764,27 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
|
|
|
37475
37764
|
const repoFullName = repos[i];
|
|
37476
37765
|
const [owner, repo] = repoFullName.split("/");
|
|
37477
37766
|
try {
|
|
37478
|
-
const
|
|
37479
|
-
|
|
37480
|
-
|
|
37481
|
-
|
|
37482
|
-
|
|
37483
|
-
|
|
37484
|
-
|
|
37485
|
-
|
|
37486
|
-
|
|
37487
|
-
|
|
37767
|
+
const labelFilters = labels.length > 0 ? labels : [void 0];
|
|
37768
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
37769
|
+
const rawIssues = [];
|
|
37770
|
+
for (const label of labelFilters) {
|
|
37771
|
+
const response = await octokit.issues.listForRepo({
|
|
37772
|
+
owner,
|
|
37773
|
+
repo,
|
|
37774
|
+
state: "open",
|
|
37775
|
+
sort: "created",
|
|
37776
|
+
direction: "desc",
|
|
37777
|
+
per_page: 5,
|
|
37778
|
+
...label !== void 0 ? { labels: label } : {}
|
|
37779
|
+
});
|
|
37780
|
+
for (const issue2 of response.data) {
|
|
37781
|
+
if (seenUrls.has(issue2.html_url))
|
|
37782
|
+
continue;
|
|
37783
|
+
seenUrls.add(issue2.html_url);
|
|
37784
|
+
rawIssues.push(issue2);
|
|
37785
|
+
}
|
|
37786
|
+
}
|
|
37787
|
+
const issuesOnly = rawIssues.filter((item) => !("pull_request" in item) && !item.assignee);
|
|
37488
37788
|
const mapped = issuesOnly.map((issue2) => ({
|
|
37489
37789
|
html_url: issue2.html_url,
|
|
37490
37790
|
repository_url: `https://api.github.com/repos/${repoFullName}`,
|
|
@@ -37509,13 +37809,13 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
|
|
|
37509
37809
|
if (isRateLimitError(error48)) {
|
|
37510
37810
|
rateLimitFailures++;
|
|
37511
37811
|
}
|
|
37512
|
-
warn(
|
|
37812
|
+
warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37513
37813
|
}
|
|
37514
37814
|
}
|
|
37515
37815
|
const allReposFailed = failedRepos === repos.length && repos.length > 0;
|
|
37516
37816
|
const rateLimitHit = rateLimitFailures > 0;
|
|
37517
37817
|
if (allReposFailed) {
|
|
37518
|
-
warn(
|
|
37818
|
+
warn(MODULE10, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
37519
37819
|
}
|
|
37520
37820
|
return { candidates, allReposFailed, rateLimitHit };
|
|
37521
37821
|
}
|
|
@@ -37572,7 +37872,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37572
37872
|
const spamRepos = detectLabelFarmingRepos(items);
|
|
37573
37873
|
if (spamRepos.size > 0) {
|
|
37574
37874
|
const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
|
|
37575
|
-
debug(
|
|
37875
|
+
debug(MODULE10, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
|
|
37576
37876
|
}
|
|
37577
37877
|
const itemsToVet = filterIssues(items).filter((item) => {
|
|
37578
37878
|
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
@@ -37583,7 +37883,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37583
37883
|
return excludedRepoSets.every((s) => !s.has(repoFullName));
|
|
37584
37884
|
}).slice(0, remainingNeeded * 2);
|
|
37585
37885
|
if (itemsToVet.length === 0) {
|
|
37586
|
-
debug(
|
|
37886
|
+
debug(MODULE10, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
|
|
37587
37887
|
return { candidates: [], allVetFailed: false, rateLimitHit: false };
|
|
37588
37888
|
}
|
|
37589
37889
|
const { candidates: results, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
|
|
@@ -37595,7 +37895,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37595
37895
|
});
|
|
37596
37896
|
const starFilteredCount = results.length - starFiltered.length;
|
|
37597
37897
|
if (starFilteredCount > 0) {
|
|
37598
|
-
debug(
|
|
37898
|
+
debug(MODULE10, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
|
|
37599
37899
|
}
|
|
37600
37900
|
return { candidates: starFiltered, allVetFailed, rateLimitHit };
|
|
37601
37901
|
}
|
|
@@ -37603,28 +37903,30 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37603
37903
|
// ../core/dist/core/personalization.js
|
|
37604
37904
|
var REPO_BOOST = 20;
|
|
37605
37905
|
var LANGUAGE_BOOST = 10;
|
|
37906
|
+
function boostScoreOf(candidate) {
|
|
37907
|
+
return candidate.personalization?.kind === "boosted" ? candidate.personalization.score : 0;
|
|
37908
|
+
}
|
|
37606
37909
|
function annotateBoost(candidates, preferLanguages, preferRepos) {
|
|
37607
37910
|
const langSet = new Set((preferLanguages ?? []).map((l) => l.trim().toLowerCase()).filter(Boolean));
|
|
37608
|
-
const repoSet = new Set((preferRepos ?? []).map((r) => r.trim()).filter(Boolean));
|
|
37911
|
+
const repoSet = new Set((preferRepos ?? []).map((r) => r.trim().toLowerCase()).filter(Boolean));
|
|
37609
37912
|
if (langSet.size === 0 && repoSet.size === 0)
|
|
37610
|
-
return;
|
|
37611
|
-
|
|
37913
|
+
return candidates;
|
|
37914
|
+
return candidates.map((c) => {
|
|
37612
37915
|
let score = 0;
|
|
37613
37916
|
const reasons = [];
|
|
37614
|
-
if (repoSet.size > 0 && repoSet.has(c.issue.repo)) {
|
|
37917
|
+
if (repoSet.size > 0 && repoSet.has(c.issue.repo.toLowerCase())) {
|
|
37615
37918
|
score += REPO_BOOST;
|
|
37616
37919
|
reasons.push(`repo affinity: ${c.issue.repo}`);
|
|
37617
37920
|
}
|
|
37618
|
-
const lang = c.projectHealth.language;
|
|
37921
|
+
const lang = c.projectHealth.checkFailed ? null : c.projectHealth.language;
|
|
37619
37922
|
if (langSet.size > 0 && lang && langSet.has(lang.toLowerCase())) {
|
|
37620
37923
|
score += LANGUAGE_BOOST;
|
|
37621
37924
|
reasons.push(`language match: ${lang}`);
|
|
37622
37925
|
}
|
|
37623
|
-
if (score
|
|
37624
|
-
c
|
|
37625
|
-
|
|
37626
|
-
|
|
37627
|
-
}
|
|
37926
|
+
if (score === 0)
|
|
37927
|
+
return c;
|
|
37928
|
+
return { ...c, personalization: { kind: "boosted", score, reasons } };
|
|
37929
|
+
});
|
|
37628
37930
|
}
|
|
37629
37931
|
function applyDiversityRatio(candidates, maxResults, diversityRatio) {
|
|
37630
37932
|
if (maxResults <= 0)
|
|
@@ -37649,10 +37951,9 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
|
|
|
37649
37951
|
break;
|
|
37650
37952
|
if (seen.has(c.issue.url))
|
|
37651
37953
|
continue;
|
|
37652
|
-
if (c
|
|
37954
|
+
if (boostScoreOf(c) > 0)
|
|
37653
37955
|
continue;
|
|
37654
|
-
|
|
37655
|
-
picks.push(c);
|
|
37956
|
+
picks.push({ ...c, personalization: { kind: "diversity" } });
|
|
37656
37957
|
seen.add(c.issue.url);
|
|
37657
37958
|
}
|
|
37658
37959
|
for (const c of candidates) {
|
|
@@ -37667,7 +37968,7 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
|
|
|
37667
37968
|
}
|
|
37668
37969
|
|
|
37669
37970
|
// ../core/dist/core/issue-discovery.js
|
|
37670
|
-
var
|
|
37971
|
+
var MODULE11 = "issue-discovery";
|
|
37671
37972
|
var LOW_BUDGET_THRESHOLD = 20;
|
|
37672
37973
|
var CRITICAL_BUDGET_THRESHOLD = 10;
|
|
37673
37974
|
function buildIssueFilter(config2) {
|
|
@@ -37676,16 +37977,17 @@ function buildIssueFilter(config2) {
|
|
|
37676
37977
|
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
37677
37978
|
if (!repoFullName)
|
|
37678
37979
|
return false;
|
|
37679
|
-
|
|
37980
|
+
const repoLower = repoFullName.toLowerCase();
|
|
37981
|
+
if (config2.excludedRepos.has(repoLower))
|
|
37680
37982
|
return false;
|
|
37681
37983
|
if (config2.excludeOrgs.size > 0) {
|
|
37682
|
-
const orgName =
|
|
37984
|
+
const orgName = repoLower.split("/")[0];
|
|
37683
37985
|
if (orgName && config2.excludeOrgs.has(orgName))
|
|
37684
37986
|
return false;
|
|
37685
37987
|
}
|
|
37686
|
-
if (config2.aiBlocklisted.has(
|
|
37988
|
+
if (config2.aiBlocklisted.has(repoLower))
|
|
37687
37989
|
return false;
|
|
37688
|
-
if (config2.lowScoringRepos.has(
|
|
37990
|
+
if (config2.lowScoringRepos.has(repoLower))
|
|
37689
37991
|
return false;
|
|
37690
37992
|
if (config2.skippedUrls.has(item.html_url))
|
|
37691
37993
|
return false;
|
|
@@ -37700,9 +38002,9 @@ function buildIssueFilter(config2) {
|
|
|
37700
38002
|
};
|
|
37701
38003
|
}
|
|
37702
38004
|
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
37703
|
-
info(
|
|
38005
|
+
info(MODULE11, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
37704
38006
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
37705
|
-
info(
|
|
38007
|
+
info(MODULE11, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
37706
38008
|
return {
|
|
37707
38009
|
candidates,
|
|
37708
38010
|
error: allReposFailed ? "All merged-PR repo fetches failed" : null,
|
|
@@ -37710,11 +38012,11 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
|
37710
38012
|
};
|
|
37711
38013
|
}
|
|
37712
38014
|
async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
|
|
37713
|
-
info(
|
|
38015
|
+
info(MODULE11, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
37714
38016
|
const phase1Labels = labels.slice(0, 3);
|
|
37715
38017
|
const reposToSearch = repos.slice(0, 10);
|
|
37716
38018
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
|
|
37717
|
-
info(
|
|
38019
|
+
info(MODULE11, `Found ${candidates.length} candidates from starred repos`);
|
|
37718
38020
|
return {
|
|
37719
38021
|
candidates,
|
|
37720
38022
|
error: allReposFailed ? "All starred repo fetches failed" : null,
|
|
@@ -37722,14 +38024,14 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
|
|
|
37722
38024
|
};
|
|
37723
38025
|
}
|
|
37724
38026
|
async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
37725
|
-
info(
|
|
38027
|
+
info(MODULE11, "Phase 2: General issue search...");
|
|
37726
38028
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37727
38029
|
const tierLabelGroups = [];
|
|
37728
38030
|
if (scopes && scopes.length > 1) {
|
|
37729
38031
|
for (const scope of scopes) {
|
|
37730
38032
|
const scopeLabels = SCOPE_LABELS[scope] ?? [];
|
|
37731
38033
|
if (scopeLabels.length === 0) {
|
|
37732
|
-
warn(
|
|
38034
|
+
warn(MODULE11, `Scope "${scope}" has no labels, skipping tier`);
|
|
37733
38035
|
continue;
|
|
37734
38036
|
}
|
|
37735
38037
|
tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
|
|
@@ -37749,7 +38051,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37749
38051
|
for (const { tier, tierLabels } of tierLabelGroups) {
|
|
37750
38052
|
try {
|
|
37751
38053
|
const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
|
|
37752
|
-
info(
|
|
38054
|
+
info(MODULE11, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
|
|
37753
38055
|
const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
|
|
37754
38056
|
tierResults.push(tierCandidates);
|
|
37755
38057
|
for (const c of tierCandidates)
|
|
@@ -37760,7 +38062,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37760
38062
|
if (vetRateLimitHit) {
|
|
37761
38063
|
rateLimitHit = true;
|
|
37762
38064
|
}
|
|
37763
|
-
info(
|
|
38065
|
+
info(MODULE11, `Found ${tierCandidates.length} candidates from ${tier} tier`);
|
|
37764
38066
|
} catch (err) {
|
|
37765
38067
|
if (getHttpStatusCode(err) === 401)
|
|
37766
38068
|
throw err;
|
|
@@ -37769,13 +38071,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37769
38071
|
if (isRateLimitError(err)) {
|
|
37770
38072
|
rateLimitHit = true;
|
|
37771
38073
|
}
|
|
37772
|
-
warn(
|
|
38074
|
+
warn(MODULE11, `Error in ${tier} tier search: ${errMsg}`);
|
|
37773
38075
|
tierResults.push([]);
|
|
37774
38076
|
}
|
|
37775
38077
|
}
|
|
37776
38078
|
const interleaved = interleaveArrays(tierResults);
|
|
37777
38079
|
if (interleaved.length === 0 && error48) {
|
|
37778
|
-
warn(
|
|
38080
|
+
warn(MODULE11, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
|
|
37779
38081
|
}
|
|
37780
38082
|
return {
|
|
37781
38083
|
candidates: interleaved.slice(0, maxResults),
|
|
@@ -37784,17 +38086,17 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37784
38086
|
};
|
|
37785
38087
|
}
|
|
37786
38088
|
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
|
|
37787
|
-
info(
|
|
38089
|
+
info(MODULE11, "Phase 3: Searching actively maintained repos...");
|
|
37788
38090
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37789
38091
|
const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
|
|
37790
38092
|
if (eligibleStarred.length > 0) {
|
|
37791
|
-
info(
|
|
38093
|
+
info(MODULE11, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
|
|
37792
38094
|
const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
|
|
37793
38095
|
if (restItems.length > 0) {
|
|
37794
38096
|
try {
|
|
37795
38097
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
|
|
37796
38098
|
if (candidates.length > 0) {
|
|
37797
|
-
info(
|
|
38099
|
+
info(MODULE11, `Found ${candidates.length} candidates from maintained-repo REST search`);
|
|
37798
38100
|
return {
|
|
37799
38101
|
candidates,
|
|
37800
38102
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37804,11 +38106,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37804
38106
|
} catch (error48) {
|
|
37805
38107
|
if (getHttpStatusCode(error48) === 401)
|
|
37806
38108
|
throw error48;
|
|
37807
|
-
warn(
|
|
38109
|
+
warn(MODULE11, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
|
|
37808
38110
|
}
|
|
37809
38111
|
}
|
|
37810
38112
|
}
|
|
37811
|
-
info(
|
|
38113
|
+
info(MODULE11, "Phase 3: Falling back to Search API...");
|
|
37812
38114
|
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
37813
38115
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
37814
38116
|
const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
|
|
@@ -37822,9 +38124,9 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37822
38124
|
order: "desc",
|
|
37823
38125
|
per_page: maxResults * 3
|
|
37824
38126
|
});
|
|
37825
|
-
info(
|
|
38127
|
+
info(MODULE11, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
37826
38128
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
|
|
37827
|
-
info(
|
|
38129
|
+
info(MODULE11, `Found ${candidates.length} candidates from maintained-repo search`);
|
|
37828
38130
|
return {
|
|
37829
38131
|
candidates,
|
|
37830
38132
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37834,7 +38136,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37834
38136
|
if (getHttpStatusCode(error48) === 401)
|
|
37835
38137
|
throw error48;
|
|
37836
38138
|
const errMsg = errorMessage(error48);
|
|
37837
|
-
warn(
|
|
38139
|
+
warn(MODULE11, `Error in maintained-repo search: ${errMsg}`);
|
|
37838
38140
|
return {
|
|
37839
38141
|
candidates: [],
|
|
37840
38142
|
error: errMsg,
|
|
@@ -37900,9 +38202,10 @@ var IssueDiscovery = class {
|
|
|
37900
38202
|
const labels = options.labels || (scopes ? buildEffectiveLabels(scopes, config2.labels) : config2.labels);
|
|
37901
38203
|
const maxResults = options.maxResults || 10;
|
|
37902
38204
|
const minStars = config2.minStars ?? 50;
|
|
37903
|
-
const interPhaseDelay = config2.interPhaseDelayMs ?? 3e4;
|
|
38205
|
+
const interPhaseDelay = options.interPhaseDelayMs ?? config2.interPhaseDelayMs ?? 3e4;
|
|
37904
38206
|
const ALL_STRATEGIES = CONCRETE_STRATEGIES;
|
|
37905
|
-
const
|
|
38207
|
+
const pickStrategies = (...candidates) => candidates.find((c) => c && c.length > 0) ?? ["all"];
|
|
38208
|
+
const rawStrategies = pickStrategies(options.strategies, config2.defaultStrategy);
|
|
37906
38209
|
const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
|
|
37907
38210
|
const strategiesUsed = [];
|
|
37908
38211
|
const allCandidates = [];
|
|
@@ -37918,18 +38221,18 @@ var IssueDiscovery = class {
|
|
|
37918
38221
|
if (rateLimit.remaining < 5) {
|
|
37919
38222
|
const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
|
|
37920
38223
|
this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
|
|
37921
|
-
warn(
|
|
38224
|
+
warn(MODULE11, this.rateLimitWarning);
|
|
37922
38225
|
}
|
|
37923
38226
|
if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
|
|
37924
|
-
info(
|
|
38227
|
+
info(MODULE11, `Search budget critical (${searchBudget} remaining) \u2014 running only Phase 0`);
|
|
37925
38228
|
} else if (searchBudget < LOW_BUDGET_THRESHOLD) {
|
|
37926
|
-
info(
|
|
38229
|
+
info(MODULE11, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
|
|
37927
38230
|
}
|
|
37928
38231
|
} catch (error48) {
|
|
37929
38232
|
if (getHttpStatusCode(error48) === 401)
|
|
37930
38233
|
throw error48;
|
|
37931
38234
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
|
|
37932
|
-
warn(
|
|
38235
|
+
warn(MODULE11, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
|
|
37933
38236
|
}
|
|
37934
38237
|
if (searchBudget <= 0) {
|
|
37935
38238
|
this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
@@ -37939,15 +38242,15 @@ var IssueDiscovery = class {
|
|
|
37939
38242
|
const openPRRepos = this.stateReader.getReposWithOpenPRs();
|
|
37940
38243
|
const starredRepos = this.getStarredRepos();
|
|
37941
38244
|
const starredRepoSet = new Set(starredRepos);
|
|
37942
|
-
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
|
|
38245
|
+
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold).map((r) => r.toLowerCase()));
|
|
37943
38246
|
const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
|
|
37944
38247
|
const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
|
|
37945
|
-
const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
|
|
38248
|
+
const aiBlocklisted = new Set(config2.aiPolicyBlocklist.map((r) => r.toLowerCase()));
|
|
37946
38249
|
if (aiBlocklisted.size > 0) {
|
|
37947
|
-
debug(
|
|
38250
|
+
debug(MODULE11, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
|
|
37948
38251
|
}
|
|
37949
38252
|
const filterIssues = buildIssueFilter({
|
|
37950
|
-
excludedRepos: new Set(config2.excludeRepos),
|
|
38253
|
+
excludedRepos: new Set(config2.excludeRepos.map((r) => r.toLowerCase())),
|
|
37951
38254
|
excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
37952
38255
|
aiBlocklisted,
|
|
37953
38256
|
lowScoringRepos,
|
|
@@ -37980,7 +38283,7 @@ var IssueDiscovery = class {
|
|
|
37980
38283
|
}
|
|
37981
38284
|
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
|
|
37982
38285
|
if (interPhaseDelay > 0) {
|
|
37983
|
-
info(
|
|
38286
|
+
info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37984
38287
|
await sleep(interPhaseDelay);
|
|
37985
38288
|
}
|
|
37986
38289
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
@@ -37992,25 +38295,26 @@ var IssueDiscovery = class {
|
|
|
37992
38295
|
phaseErrors["1"] = result.error;
|
|
37993
38296
|
if (result.rateLimitHit)
|
|
37994
38297
|
rateLimitHitDuringSearch = true;
|
|
38298
|
+
strategiesUsed.push("starred");
|
|
37995
38299
|
}
|
|
37996
38300
|
}
|
|
37997
|
-
strategiesUsed.push("starred");
|
|
37998
38301
|
}
|
|
37999
|
-
const broadDelay = config2.broadPhaseDelayMs ?? 9e4;
|
|
38000
|
-
const
|
|
38302
|
+
const broadDelay = options.broadPhaseDelayMs ?? config2.broadPhaseDelayMs ?? 9e4;
|
|
38303
|
+
const configuredSkipThreshold = config2.skipBroadWhenSufficientResults ?? 8;
|
|
38304
|
+
const skipThreshold = configuredSkipThreshold > 0 ? Math.min(configuredSkipThreshold, maxResults - 1) : 0;
|
|
38001
38305
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
|
|
38002
38306
|
if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
|
|
38003
|
-
info(
|
|
38307
|
+
info(MODULE11, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
|
|
38004
38308
|
} else {
|
|
38005
38309
|
if (interPhaseDelay > 0) {
|
|
38006
|
-
info(
|
|
38310
|
+
info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
38007
38311
|
await sleep(interPhaseDelay);
|
|
38008
38312
|
}
|
|
38009
38313
|
if (allCandidates.length > 0 && broadDelay > 0) {
|
|
38010
|
-
info(
|
|
38314
|
+
info(MODULE11, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
|
|
38011
38315
|
await sleep(broadDelay);
|
|
38012
38316
|
} else if (allCandidates.length === 0) {
|
|
38013
|
-
info(
|
|
38317
|
+
info(MODULE11, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
38014
38318
|
}
|
|
38015
38319
|
const remaining = maxResults - allCandidates.length;
|
|
38016
38320
|
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
@@ -38018,12 +38322,12 @@ var IssueDiscovery = class {
|
|
|
38018
38322
|
phaseErrors["2"] = result.error;
|
|
38019
38323
|
if (result.rateLimitHit)
|
|
38020
38324
|
rateLimitHitDuringSearch = true;
|
|
38325
|
+
strategiesUsed.push("broad");
|
|
38021
38326
|
}
|
|
38022
|
-
strategiesUsed.push("broad");
|
|
38023
38327
|
}
|
|
38024
38328
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
|
|
38025
38329
|
if (interPhaseDelay > 0) {
|
|
38026
|
-
info(
|
|
38330
|
+
info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
38027
38331
|
await sleep(interPhaseDelay);
|
|
38028
38332
|
}
|
|
38029
38333
|
const remaining = maxResults - allCandidates.length;
|
|
@@ -38058,8 +38362,8 @@ var IssueDiscovery = class {
|
|
|
38058
38362
|
if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
|
|
38059
38363
|
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.`;
|
|
38060
38364
|
}
|
|
38061
|
-
annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
|
|
38062
|
-
|
|
38365
|
+
const ranked = annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
|
|
38366
|
+
ranked.sort((a, b) => {
|
|
38063
38367
|
const priorityOrder = {
|
|
38064
38368
|
merged_pr: 0,
|
|
38065
38369
|
starred: 1,
|
|
@@ -38072,14 +38376,14 @@ var IssueDiscovery = class {
|
|
|
38072
38376
|
const recDiff = recommendationOrder[a.recommendation] - recommendationOrder[b.recommendation];
|
|
38073
38377
|
if (recDiff !== 0)
|
|
38074
38378
|
return recDiff;
|
|
38075
|
-
const boostDiff = (b
|
|
38379
|
+
const boostDiff = boostScoreOf(b) - boostScoreOf(a);
|
|
38076
38380
|
if (boostDiff !== 0)
|
|
38077
38381
|
return boostDiff;
|
|
38078
38382
|
return b.viabilityScore - a.viabilityScore;
|
|
38079
38383
|
});
|
|
38080
|
-
const capped = applyPerRepoCap(
|
|
38384
|
+
const capped = applyPerRepoCap(ranked, 2);
|
|
38081
38385
|
const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
|
|
38082
|
-
info(
|
|
38386
|
+
info(MODULE11, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${finalPicks.length} candidates returned`);
|
|
38083
38387
|
return { candidates: finalPicks, strategiesUsed };
|
|
38084
38388
|
}
|
|
38085
38389
|
/**
|
|
@@ -38112,7 +38416,7 @@ var IssueDiscovery = class {
|
|
|
38112
38416
|
};
|
|
38113
38417
|
|
|
38114
38418
|
// ../core/dist/core/roadmap.js
|
|
38115
|
-
var
|
|
38419
|
+
var MODULE12 = "roadmap";
|
|
38116
38420
|
var CACHE_TTL_MS2 = 60 * 60 * 1e3;
|
|
38117
38421
|
var ROADMAP_PATHS = [
|
|
38118
38422
|
"ROADMAP.md",
|
|
@@ -38166,6 +38470,19 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
38166
38470
|
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS2) {
|
|
38167
38471
|
return cached2.refs;
|
|
38168
38472
|
}
|
|
38473
|
+
const inflight = roadmapInflight.get(cacheKey);
|
|
38474
|
+
if (inflight)
|
|
38475
|
+
return inflight;
|
|
38476
|
+
const promise2 = fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey);
|
|
38477
|
+
roadmapInflight.set(cacheKey, promise2);
|
|
38478
|
+
try {
|
|
38479
|
+
return await promise2;
|
|
38480
|
+
} finally {
|
|
38481
|
+
roadmapInflight.delete(cacheKey);
|
|
38482
|
+
}
|
|
38483
|
+
}
|
|
38484
|
+
var roadmapInflight = /* @__PURE__ */ new Map();
|
|
38485
|
+
async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
|
|
38169
38486
|
for (const path5 of ROADMAP_PATHS) {
|
|
38170
38487
|
try {
|
|
38171
38488
|
const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
|
|
@@ -38177,12 +38494,11 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
38177
38494
|
pruneCache2();
|
|
38178
38495
|
return refs;
|
|
38179
38496
|
} catch (err) {
|
|
38180
|
-
|
|
38181
|
-
throw err;
|
|
38497
|
+
rethrowIfFatal(err);
|
|
38182
38498
|
const status = getHttpStatusCode(err);
|
|
38183
38499
|
if (status === 404)
|
|
38184
38500
|
continue;
|
|
38185
|
-
warn(
|
|
38501
|
+
warn(MODULE12, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(err)}`);
|
|
38186
38502
|
}
|
|
38187
38503
|
}
|
|
38188
38504
|
const empty = /* @__PURE__ */ new Set();
|
|
@@ -38192,7 +38508,7 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
38192
38508
|
}
|
|
38193
38509
|
|
|
38194
38510
|
// ../core/dist/core/feature-discovery.js
|
|
38195
|
-
var
|
|
38511
|
+
var MODULE13 = "feature-discovery";
|
|
38196
38512
|
var INTER_REPO_DELAY_MS = 2e3;
|
|
38197
38513
|
var MIN_VIABILITY_SCORE = 40;
|
|
38198
38514
|
var ANCHOR_THRESHOLD = 3;
|
|
@@ -38284,6 +38600,37 @@ function isFeatureIssue(item) {
|
|
|
38284
38600
|
return false;
|
|
38285
38601
|
return labels.some((l) => FEATURE_LABELS.includes(l));
|
|
38286
38602
|
}
|
|
38603
|
+
async function vetAndClassify(item, vetter, roadmapRefs) {
|
|
38604
|
+
const labels = extractLabels(item);
|
|
38605
|
+
const hasMilestone = !!item.milestone;
|
|
38606
|
+
const reactions = item.reactions?.total_count ?? 0;
|
|
38607
|
+
const comments = item.comments ?? 0;
|
|
38608
|
+
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38609
|
+
const useRoadmap = roadmapRefs !== void 0;
|
|
38610
|
+
const onRoadmap = useRoadmap && typeof item.number === "number" && roadmapRefs.has(item.number);
|
|
38611
|
+
let candidate;
|
|
38612
|
+
try {
|
|
38613
|
+
candidate = await vetter.vetIssue(item.html_url, {
|
|
38614
|
+
featureSignals: {
|
|
38615
|
+
reactions,
|
|
38616
|
+
comments,
|
|
38617
|
+
hasMilestone,
|
|
38618
|
+
wontfixNoContributor,
|
|
38619
|
+
...useRoadmap ? { onRoadmap } : {}
|
|
38620
|
+
}
|
|
38621
|
+
});
|
|
38622
|
+
} catch (err) {
|
|
38623
|
+
rethrowIfFatal(err);
|
|
38624
|
+
warn(MODULE13, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38625
|
+
return null;
|
|
38626
|
+
}
|
|
38627
|
+
const horizon = classifyHorizon({
|
|
38628
|
+
hasMilestone,
|
|
38629
|
+
labels,
|
|
38630
|
+
...useRoadmap ? { isOnRoadmap: onRoadmap } : {}
|
|
38631
|
+
});
|
|
38632
|
+
return { ...candidate, horizon };
|
|
38633
|
+
}
|
|
38287
38634
|
async function discoverFeatures(opts) {
|
|
38288
38635
|
const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold);
|
|
38289
38636
|
if (anchorRepos.length === 0) {
|
|
@@ -38316,42 +38663,15 @@ async function discoverFeatures(opts) {
|
|
|
38316
38663
|
response = listResp;
|
|
38317
38664
|
roadmapRefs = refs;
|
|
38318
38665
|
} catch (err) {
|
|
38319
|
-
|
|
38320
|
-
|
|
38321
|
-
warn(MODULE12, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
|
|
38666
|
+
rethrowIfFatal(err);
|
|
38667
|
+
warn(MODULE13, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
|
|
38322
38668
|
continue;
|
|
38323
38669
|
}
|
|
38324
38670
|
const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
|
|
38325
38671
|
for (const item of items) {
|
|
38326
|
-
const
|
|
38327
|
-
|
|
38328
|
-
|
|
38329
|
-
const comments = item.comments ?? 0;
|
|
38330
|
-
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38331
|
-
const onRoadmap = typeof item.number === "number" && roadmapRefs.has(item.number);
|
|
38332
|
-
let candidate;
|
|
38333
|
-
try {
|
|
38334
|
-
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38335
|
-
featureSignals: {
|
|
38336
|
-
reactions,
|
|
38337
|
-
comments,
|
|
38338
|
-
hasMilestone,
|
|
38339
|
-
wontfixNoContributor,
|
|
38340
|
-
onRoadmap
|
|
38341
|
-
}
|
|
38342
|
-
});
|
|
38343
|
-
} catch (err) {
|
|
38344
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38345
|
-
throw err;
|
|
38346
|
-
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38347
|
-
continue;
|
|
38348
|
-
}
|
|
38349
|
-
const horizon = classifyHorizon({
|
|
38350
|
-
hasMilestone,
|
|
38351
|
-
labels,
|
|
38352
|
-
isOnRoadmap: onRoadmap
|
|
38353
|
-
});
|
|
38354
|
-
candidates.push({ ...candidate, horizon });
|
|
38672
|
+
const candidate = await vetAndClassify(item, opts.vetter, roadmapRefs);
|
|
38673
|
+
if (candidate)
|
|
38674
|
+
candidates.push(candidate);
|
|
38355
38675
|
}
|
|
38356
38676
|
}
|
|
38357
38677
|
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
@@ -38372,10 +38692,8 @@ function buildBroadFeatureSearchQuery(opts) {
|
|
|
38372
38692
|
for (const excl of FEATURE_EXCLUSION_LABELS) {
|
|
38373
38693
|
parts.push(`-label:"${excl}"`);
|
|
38374
38694
|
}
|
|
38375
|
-
|
|
38376
|
-
|
|
38377
|
-
const langClause = languages.map((l) => `language:${l}`).join(" OR ");
|
|
38378
|
-
parts.push(`(${langClause})`);
|
|
38695
|
+
if (opts.language) {
|
|
38696
|
+
parts.push(`language:${opts.language}`);
|
|
38379
38697
|
}
|
|
38380
38698
|
for (const repo of opts.excludeRepos ?? []) {
|
|
38381
38699
|
parts.push(`-repo:${repo}`);
|
|
@@ -38385,8 +38703,19 @@ function buildBroadFeatureSearchQuery(opts) {
|
|
|
38385
38703
|
}
|
|
38386
38704
|
return parts.join(" ");
|
|
38387
38705
|
}
|
|
38706
|
+
function buildBroadFeatureSearchQueries(opts) {
|
|
38707
|
+
const base = {
|
|
38708
|
+
excludeRepos: opts.excludeRepos,
|
|
38709
|
+
excludeOrgs: opts.excludeOrgs
|
|
38710
|
+
};
|
|
38711
|
+
const languages = (opts.languages ?? []).filter((l) => l && l.toLowerCase() !== "any");
|
|
38712
|
+
if (languages.length === 0) {
|
|
38713
|
+
return [buildBroadFeatureSearchQuery(base)];
|
|
38714
|
+
}
|
|
38715
|
+
return languages.map((language) => buildBroadFeatureSearchQuery({ ...base, language }));
|
|
38716
|
+
}
|
|
38388
38717
|
async function discoverFeaturesBroad(opts) {
|
|
38389
|
-
const
|
|
38718
|
+
const queries = buildBroadFeatureSearchQueries({
|
|
38390
38719
|
languages: opts.languages,
|
|
38391
38720
|
excludeRepos: opts.excludeRepos,
|
|
38392
38721
|
excludeOrgs: opts.excludeOrgs
|
|
@@ -38394,17 +38723,26 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38394
38723
|
const maxToVet = opts.maxToVet ?? DEFAULT_BROAD_MAX_TO_VET;
|
|
38395
38724
|
let items;
|
|
38396
38725
|
try {
|
|
38397
|
-
const
|
|
38398
|
-
|
|
38399
|
-
|
|
38400
|
-
|
|
38401
|
-
|
|
38402
|
-
|
|
38403
|
-
|
|
38726
|
+
const merged = [];
|
|
38727
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
38728
|
+
for (const query of queries) {
|
|
38729
|
+
const data = await cachedSearchIssues(opts.octokit, {
|
|
38730
|
+
q: query,
|
|
38731
|
+
sort: "interactions",
|
|
38732
|
+
order: "desc",
|
|
38733
|
+
per_page: maxToVet
|
|
38734
|
+
});
|
|
38735
|
+
for (const raw of data.items) {
|
|
38736
|
+
if (seenUrls.has(raw.html_url))
|
|
38737
|
+
continue;
|
|
38738
|
+
seenUrls.add(raw.html_url);
|
|
38739
|
+
merged.push(raw);
|
|
38740
|
+
}
|
|
38741
|
+
}
|
|
38742
|
+
items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).slice(0, maxToVet);
|
|
38404
38743
|
} catch (err) {
|
|
38405
|
-
|
|
38406
|
-
|
|
38407
|
-
warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
|
|
38744
|
+
rethrowIfFatal(err);
|
|
38745
|
+
warn(MODULE13, `broad feature search failed: ${errorMessage(err)}`);
|
|
38408
38746
|
return {
|
|
38409
38747
|
quickWins: [],
|
|
38410
38748
|
biggerBets: [],
|
|
@@ -38414,32 +38752,9 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38414
38752
|
}
|
|
38415
38753
|
const candidates = [];
|
|
38416
38754
|
for (const item of items) {
|
|
38417
|
-
const
|
|
38418
|
-
|
|
38419
|
-
|
|
38420
|
-
const comments = item.comments ?? 0;
|
|
38421
|
-
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38422
|
-
let candidate;
|
|
38423
|
-
try {
|
|
38424
|
-
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38425
|
-
featureSignals: {
|
|
38426
|
-
reactions,
|
|
38427
|
-
comments,
|
|
38428
|
-
hasMilestone,
|
|
38429
|
-
wontfixNoContributor
|
|
38430
|
-
// Roadmap scraping is per-repo and would require an extra fetch
|
|
38431
|
-
// per unique repo in the broad result set — deliberately skipped
|
|
38432
|
-
// here to keep the broad path cheap. Anchor mode keeps the bonus.
|
|
38433
|
-
}
|
|
38434
|
-
});
|
|
38435
|
-
} catch (err) {
|
|
38436
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38437
|
-
throw err;
|
|
38438
|
-
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38439
|
-
continue;
|
|
38440
|
-
}
|
|
38441
|
-
const horizon = classifyHorizon({ hasMilestone, labels });
|
|
38442
|
-
candidates.push({ ...candidate, horizon });
|
|
38755
|
+
const candidate = await vetAndClassify(item, opts.vetter);
|
|
38756
|
+
if (candidate)
|
|
38757
|
+
candidates.push(candidate);
|
|
38443
38758
|
}
|
|
38444
38759
|
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
38445
38760
|
const split = splitByHorizon(passing, opts.count, opts.splitRatio);
|
|
@@ -38454,19 +38769,16 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38454
38769
|
// ../core/dist/core/gist-state-store.js
|
|
38455
38770
|
var fs3 = __toESM(require("fs"), 1);
|
|
38456
38771
|
var path3 = __toESM(require("path"), 1);
|
|
38457
|
-
var
|
|
38772
|
+
var MODULE14 = "gist-state";
|
|
38458
38773
|
var GIST_DESCRIPTION = "oss-scout-state";
|
|
38459
38774
|
var GIST_FILENAME = "state.json";
|
|
38460
38775
|
var GIST_ID_FILE = "gist-id";
|
|
38461
38776
|
var CACHE_FILE = "state-cache.json";
|
|
38462
|
-
var SEARCH_MAX_PAGES =
|
|
38777
|
+
var SEARCH_MAX_PAGES = 10;
|
|
38463
38778
|
function classifyDegradedReason(err) {
|
|
38464
38779
|
if (isRateLimitError(err))
|
|
38465
38780
|
return "rate_limit";
|
|
38466
38781
|
const status = getHttpStatusCode(err);
|
|
38467
|
-
if (status === 403 && errorMessage(err).toLowerCase().includes("abuse detection")) {
|
|
38468
|
-
return "rate_limit";
|
|
38469
|
-
}
|
|
38470
38782
|
if (status !== void 0 && status >= 500 && status < 600)
|
|
38471
38783
|
return "server";
|
|
38472
38784
|
if (err && typeof err === "object" && "code" in err) {
|
|
@@ -38502,7 +38814,7 @@ var GistStateStore = class {
|
|
|
38502
38814
|
} catch (err) {
|
|
38503
38815
|
if (getHttpStatusCode(err) === 401)
|
|
38504
38816
|
throw err;
|
|
38505
|
-
warn(
|
|
38817
|
+
warn(MODULE14, `API bootstrap failed: ${errorMessage(err)}`);
|
|
38506
38818
|
return this.bootstrapFromCache(classifyDegradedReason(err));
|
|
38507
38819
|
}
|
|
38508
38820
|
}
|
|
@@ -38510,14 +38822,25 @@ var GistStateStore = class {
|
|
|
38510
38822
|
* Push state to the gist. Also writes to local cache as fallback.
|
|
38511
38823
|
*/
|
|
38512
38824
|
async push(state) {
|
|
38513
|
-
this.writeCache(state);
|
|
38514
38825
|
if (!this.gistId) {
|
|
38515
|
-
warn(
|
|
38826
|
+
warn(MODULE14, "No gist ID \u2014 cannot push");
|
|
38827
|
+
this.writeCache(state);
|
|
38516
38828
|
return false;
|
|
38517
38829
|
}
|
|
38518
|
-
|
|
38830
|
+
let toWrite = state;
|
|
38831
|
+
try {
|
|
38832
|
+
const remote = await this.fetchGistState(this.gistId);
|
|
38833
|
+
if (remote) {
|
|
38834
|
+
toWrite = mergeStates(state, remote);
|
|
38835
|
+
}
|
|
38836
|
+
} catch (err) {
|
|
38837
|
+
rethrowIfFatal(err);
|
|
38838
|
+
warn(MODULE14, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
|
|
38839
|
+
}
|
|
38840
|
+
this.writeCache(toWrite);
|
|
38841
|
+
const json2 = JSON.stringify(toWrite, null, 2);
|
|
38519
38842
|
if (json2.length > 9e5) {
|
|
38520
|
-
warn(
|
|
38843
|
+
warn(MODULE14, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
38521
38844
|
return false;
|
|
38522
38845
|
}
|
|
38523
38846
|
try {
|
|
@@ -38527,34 +38850,14 @@ var GistStateStore = class {
|
|
|
38527
38850
|
[GIST_FILENAME]: { content: json2 }
|
|
38528
38851
|
}
|
|
38529
38852
|
});
|
|
38530
|
-
debug(
|
|
38853
|
+
debug(MODULE14, "State pushed to gist");
|
|
38531
38854
|
return true;
|
|
38532
38855
|
} catch (err) {
|
|
38533
|
-
|
|
38534
|
-
|
|
38535
|
-
warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
|
|
38856
|
+
rethrowIfFatal(err);
|
|
38857
|
+
warn(MODULE14, `Failed to push: ${errorMessage(err)}`);
|
|
38536
38858
|
return false;
|
|
38537
38859
|
}
|
|
38538
38860
|
}
|
|
38539
|
-
/**
|
|
38540
|
-
* Pull state from the gist and merge with local state.
|
|
38541
|
-
*/
|
|
38542
|
-
async pull() {
|
|
38543
|
-
if (!this.gistId)
|
|
38544
|
-
return null;
|
|
38545
|
-
try {
|
|
38546
|
-
const state = await this.fetchGistState(this.gistId);
|
|
38547
|
-
if (state) {
|
|
38548
|
-
this.writeCache(state);
|
|
38549
|
-
}
|
|
38550
|
-
return state;
|
|
38551
|
-
} catch (err) {
|
|
38552
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38553
|
-
throw err;
|
|
38554
|
-
warn(MODULE13, `Failed to pull: ${errorMessage(err)}`);
|
|
38555
|
-
return null;
|
|
38556
|
-
}
|
|
38557
|
-
}
|
|
38558
38861
|
/** Get the current gist ID (if known). */
|
|
38559
38862
|
getGistId() {
|
|
38560
38863
|
return this.gistId;
|
|
@@ -38563,35 +38866,40 @@ var GistStateStore = class {
|
|
|
38563
38866
|
async bootstrapFromApi() {
|
|
38564
38867
|
const cachedId = this.readCachedGistId();
|
|
38565
38868
|
if (cachedId) {
|
|
38566
|
-
debug(
|
|
38869
|
+
debug(MODULE14, `Trying cached gist ID: ${cachedId}`);
|
|
38567
38870
|
try {
|
|
38568
|
-
const
|
|
38569
|
-
if (
|
|
38871
|
+
const fetched = await this.fetchGistState(cachedId);
|
|
38872
|
+
if (fetched) {
|
|
38570
38873
|
this.gistId = cachedId;
|
|
38874
|
+
const state = this.mergeCacheInto(fetched);
|
|
38571
38875
|
this.writeCache(state);
|
|
38572
38876
|
return { gistId: cachedId, state, created: false };
|
|
38573
38877
|
}
|
|
38574
38878
|
} catch (err) {
|
|
38575
38879
|
if (getHttpStatusCode(err) !== 404)
|
|
38576
38880
|
throw err;
|
|
38577
|
-
debug(
|
|
38578
|
-
}
|
|
38579
|
-
debug(
|
|
38580
|
-
}
|
|
38581
|
-
const
|
|
38582
|
-
if (
|
|
38583
|
-
debug(
|
|
38584
|
-
this.saveGistId(
|
|
38585
|
-
this.gistId =
|
|
38586
|
-
const
|
|
38587
|
-
if (
|
|
38881
|
+
debug(MODULE14, `Cached gist gone (404): ${errorMessage(err)}`);
|
|
38882
|
+
}
|
|
38883
|
+
debug(MODULE14, "Cached gist ID invalid, searching...");
|
|
38884
|
+
}
|
|
38885
|
+
const search = await this.searchForGist();
|
|
38886
|
+
if (search.id) {
|
|
38887
|
+
debug(MODULE14, `Found gist via search: ${search.id}`);
|
|
38888
|
+
this.saveGistId(search.id);
|
|
38889
|
+
this.gistId = search.id;
|
|
38890
|
+
const fetched = await this.fetchGistState(search.id);
|
|
38891
|
+
if (fetched) {
|
|
38892
|
+
const state = this.mergeCacheInto(fetched);
|
|
38588
38893
|
this.writeCache(state);
|
|
38589
|
-
return { gistId:
|
|
38894
|
+
return { gistId: search.id, state, created: false };
|
|
38590
38895
|
}
|
|
38591
|
-
warn(
|
|
38896
|
+
warn(MODULE14, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
|
|
38592
38897
|
return this.bootstrapFromCache("unknown");
|
|
38593
38898
|
}
|
|
38594
|
-
|
|
38899
|
+
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.`);
|
|
38901
|
+
}
|
|
38902
|
+
debug(MODULE14, "No existing gist found, creating new one");
|
|
38595
38903
|
const freshState = ScoutStateSchema.parse({ version: 1 });
|
|
38596
38904
|
const newId = await this.createGist(freshState);
|
|
38597
38905
|
this.saveGistId(newId);
|
|
@@ -38602,7 +38910,7 @@ var GistStateStore = class {
|
|
|
38602
38910
|
bootstrapFromCache(reason) {
|
|
38603
38911
|
const cached2 = this.readCache();
|
|
38604
38912
|
if (cached2) {
|
|
38605
|
-
debug(
|
|
38913
|
+
debug(MODULE14, "Bootstrapped from local cache (degraded mode)");
|
|
38606
38914
|
const cachedId = this.readCachedGistId();
|
|
38607
38915
|
if (cachedId)
|
|
38608
38916
|
this.gistId = cachedId;
|
|
@@ -38614,7 +38922,7 @@ var GistStateStore = class {
|
|
|
38614
38922
|
degradedReason: reason
|
|
38615
38923
|
};
|
|
38616
38924
|
}
|
|
38617
|
-
debug(
|
|
38925
|
+
debug(MODULE14, "No cache available, using fresh state (degraded mode)");
|
|
38618
38926
|
const fresh = ScoutStateSchema.parse({ version: 1 });
|
|
38619
38927
|
return {
|
|
38620
38928
|
gistId: "",
|
|
@@ -38632,12 +38940,17 @@ var GistStateStore = class {
|
|
|
38632
38940
|
return null;
|
|
38633
38941
|
try {
|
|
38634
38942
|
const parsed = JSON.parse(file2.content);
|
|
38635
|
-
return
|
|
38943
|
+
return parseScoutState(parsed);
|
|
38636
38944
|
} catch (err) {
|
|
38637
|
-
warn(
|
|
38945
|
+
warn(MODULE14, `Gist content failed validation: ${errorMessage(err)}`);
|
|
38638
38946
|
return null;
|
|
38639
38947
|
}
|
|
38640
38948
|
}
|
|
38949
|
+
/**
|
|
38950
|
+
* Scan the user's gists for the state gist. `exhaustive: false` means the
|
|
38951
|
+
* page cap was hit while pages were still full, so the account may hold
|
|
38952
|
+
* the state gist beyond the scan window.
|
|
38953
|
+
*/
|
|
38641
38954
|
async searchForGist() {
|
|
38642
38955
|
for (let page = 1; page <= SEARCH_MAX_PAGES; page++) {
|
|
38643
38956
|
const { data: gists } = await this.octokit.gists.list({
|
|
@@ -38645,12 +38958,14 @@ var GistStateStore = class {
|
|
|
38645
38958
|
page
|
|
38646
38959
|
});
|
|
38647
38960
|
if (gists.length === 0)
|
|
38648
|
-
|
|
38961
|
+
return { id: null, exhaustive: true };
|
|
38649
38962
|
const match = gists.find((g) => g.description === GIST_DESCRIPTION);
|
|
38650
38963
|
if (match)
|
|
38651
|
-
return match.id;
|
|
38964
|
+
return { id: match.id, exhaustive: true };
|
|
38965
|
+
if (gists.length < 100)
|
|
38966
|
+
return { id: null, exhaustive: true };
|
|
38652
38967
|
}
|
|
38653
|
-
return null;
|
|
38968
|
+
return { id: null, exhaustive: false };
|
|
38654
38969
|
}
|
|
38655
38970
|
async createGist(state) {
|
|
38656
38971
|
const { data } = await this.octokit.gists.create({
|
|
@@ -38670,7 +38985,7 @@ var GistStateStore = class {
|
|
|
38670
38985
|
} catch (err) {
|
|
38671
38986
|
const code = err?.code;
|
|
38672
38987
|
if (code !== "ENOENT") {
|
|
38673
|
-
warn(
|
|
38988
|
+
warn(MODULE14, `Failed to read cached gist ID: ${errorMessage(err)}`);
|
|
38674
38989
|
}
|
|
38675
38990
|
return null;
|
|
38676
38991
|
}
|
|
@@ -38678,14 +38993,25 @@ var GistStateStore = class {
|
|
|
38678
38993
|
saveGistId(id) {
|
|
38679
38994
|
fs3.writeFileSync(getGistIdPath(), id + "\n", { mode: 384 });
|
|
38680
38995
|
}
|
|
38996
|
+
/**
|
|
38997
|
+
* Merge the local state-cache into a freshly fetched gist state before it
|
|
38998
|
+
* overwrites the cache (#117). Without this, a prior failed push left its
|
|
38999
|
+
* only copy of the user's changes in state-cache.json, which the next
|
|
39000
|
+
* successful bootstrap silently clobbered. Tombstone-aware mergeStates
|
|
39001
|
+
* keeps both sides' real changes.
|
|
39002
|
+
*/
|
|
39003
|
+
mergeCacheInto(fetched) {
|
|
39004
|
+
const cached2 = this.readCache();
|
|
39005
|
+
return cached2 ? mergeStates(cached2, fetched) : fetched;
|
|
39006
|
+
}
|
|
38681
39007
|
readCache() {
|
|
38682
39008
|
try {
|
|
38683
39009
|
const raw = fs3.readFileSync(getCachePath(), "utf-8");
|
|
38684
|
-
return
|
|
39010
|
+
return parseScoutState(JSON.parse(raw));
|
|
38685
39011
|
} catch (err) {
|
|
38686
39012
|
const code = err?.code;
|
|
38687
39013
|
if (code !== "ENOENT") {
|
|
38688
|
-
warn(
|
|
39014
|
+
warn(MODULE14, `Failed to read state cache: ${errorMessage(err)}`);
|
|
38689
39015
|
}
|
|
38690
39016
|
return null;
|
|
38691
39017
|
}
|
|
@@ -38696,22 +39022,61 @@ var GistStateStore = class {
|
|
|
38696
39022
|
mode: 384
|
|
38697
39023
|
});
|
|
38698
39024
|
} catch (err) {
|
|
38699
|
-
warn(
|
|
39025
|
+
warn(MODULE14, `Failed to write cache: ${errorMessage(err)}`);
|
|
38700
39026
|
}
|
|
38701
39027
|
}
|
|
38702
39028
|
};
|
|
39029
|
+
var TOMBSTONE_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
39030
|
+
function mergeTombstones(local, remote) {
|
|
39031
|
+
const cutoff = Date.now() - TOMBSTONE_TTL_MS;
|
|
39032
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
39033
|
+
for (const t of [...local, ...remote]) {
|
|
39034
|
+
const existing = byUrl.get(t.url);
|
|
39035
|
+
if (!existing || t.removedAt > existing.removedAt)
|
|
39036
|
+
byUrl.set(t.url, t);
|
|
39037
|
+
}
|
|
39038
|
+
return [...byUrl.values()].filter((t) => {
|
|
39039
|
+
const ts = new Date(t.removedAt).getTime();
|
|
39040
|
+
return !Number.isFinite(ts) || ts >= cutoff;
|
|
39041
|
+
});
|
|
39042
|
+
}
|
|
39043
|
+
function applyTombstones(items, tombstones, urlOf, touchedAtOf) {
|
|
39044
|
+
if (tombstones.length === 0)
|
|
39045
|
+
return items;
|
|
39046
|
+
const byUrl = new Map(tombstones.map((t) => [t.url, t.removedAt]));
|
|
39047
|
+
return items.filter((item) => {
|
|
39048
|
+
const removedAt = byUrl.get(urlOf(item));
|
|
39049
|
+
if (removedAt === void 0)
|
|
39050
|
+
return true;
|
|
39051
|
+
return touchedAtOf(item) > removedAt;
|
|
39052
|
+
});
|
|
39053
|
+
}
|
|
38703
39054
|
function mergeStates(local, remote) {
|
|
39055
|
+
const tombstones = mergeTombstones(local.tombstones ?? [], remote.tombstones ?? []);
|
|
39056
|
+
const savedResults = applyTombstones(mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []), tombstones, (r) => r.issueUrl, (r) => r.lastSeenAt);
|
|
39057
|
+
const skippedIssues = applyTombstones(mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []), tombstones, (s) => s.url, (s) => s.skippedAt);
|
|
39058
|
+
const skippedUrls = new Set(skippedIssues.map((s) => s.url));
|
|
39059
|
+
const savedResultsFinal = savedResults.filter((r) => !skippedUrls.has(r.issueUrl));
|
|
39060
|
+
const localPrefsTs = local.preferencesUpdatedAt;
|
|
39061
|
+
const remotePrefsTs = remote.preferencesUpdatedAt;
|
|
39062
|
+
const localPrefsWin = localPrefsTs !== void 0 && (remotePrefsTs === void 0 || localPrefsTs > remotePrefsTs);
|
|
39063
|
+
const preferences = localPrefsWin ? local.preferences : remote.preferences;
|
|
39064
|
+
const preferencesUpdatedAt = pickFresherTimestamp(localPrefsTs, remotePrefsTs);
|
|
38704
39065
|
return {
|
|
39066
|
+
...local,
|
|
39067
|
+
...remote,
|
|
38705
39068
|
version: 1,
|
|
38706
|
-
preferences
|
|
39069
|
+
preferences,
|
|
39070
|
+
preferencesUpdatedAt,
|
|
39071
|
+
tombstones,
|
|
38707
39072
|
repoScores: mergeRepoScores(local.repoScores, remote.repoScores),
|
|
38708
39073
|
starredRepos: mergeStarredRepos(local, remote),
|
|
38709
39074
|
starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
|
|
38710
39075
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
38711
39076
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
38712
39077
|
openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
|
|
38713
|
-
savedResults:
|
|
38714
|
-
skippedIssues
|
|
39078
|
+
savedResults: savedResultsFinal,
|
|
39079
|
+
skippedIssues,
|
|
38715
39080
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
38716
39081
|
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
38717
39082
|
gistId: remote.gistId ?? local.gistId
|
|
@@ -38785,7 +39150,7 @@ function pickFresherTimestamp(a, b) {
|
|
|
38785
39150
|
// ../core/dist/core/local-state.js
|
|
38786
39151
|
var fs4 = __toESM(require("fs"), 1);
|
|
38787
39152
|
var path4 = __toESM(require("path"), 1);
|
|
38788
|
-
var
|
|
39153
|
+
var MODULE15 = "local-state";
|
|
38789
39154
|
function getStatePath() {
|
|
38790
39155
|
return path4.join(getDataDir(), "state.json");
|
|
38791
39156
|
}
|
|
@@ -38793,23 +39158,40 @@ function loadLocalState() {
|
|
|
38793
39158
|
const statePath = getStatePath();
|
|
38794
39159
|
try {
|
|
38795
39160
|
const raw = fs4.readFileSync(statePath, "utf-8");
|
|
38796
|
-
return
|
|
39161
|
+
return parseScoutState(JSON.parse(raw));
|
|
38797
39162
|
} catch (err) {
|
|
38798
39163
|
const code = err?.code;
|
|
38799
39164
|
if (code === "ENOENT") {
|
|
38800
39165
|
return ScoutStateSchema.parse({ version: 1 });
|
|
38801
39166
|
}
|
|
38802
|
-
warn(
|
|
39167
|
+
warn(MODULE15, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
|
|
38803
39168
|
try {
|
|
38804
39169
|
const backupPath = `${statePath}.corrupt.${Date.now()}`;
|
|
38805
39170
|
fs4.copyFileSync(statePath, backupPath);
|
|
38806
|
-
warn(
|
|
39171
|
+
warn(MODULE15, `Corrupt state backed up to ${backupPath}`);
|
|
38807
39172
|
} catch (backupErr) {
|
|
38808
|
-
warn(
|
|
39173
|
+
warn(MODULE15, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
38809
39174
|
}
|
|
38810
39175
|
return ScoutStateSchema.parse({ version: 1 });
|
|
38811
39176
|
}
|
|
38812
39177
|
}
|
|
39178
|
+
var tmpCounter = 0;
|
|
39179
|
+
function saveLocalState(state) {
|
|
39180
|
+
const statePath = getStatePath();
|
|
39181
|
+
const tmpPath = `${statePath}.tmp.${process.pid}.${tmpCounter++}`;
|
|
39182
|
+
const data = JSON.stringify(state, null, 2) + "\n";
|
|
39183
|
+
fs4.writeFileSync(tmpPath, data, { mode: 384 });
|
|
39184
|
+
try {
|
|
39185
|
+
fs4.renameSync(tmpPath, statePath);
|
|
39186
|
+
} catch (err) {
|
|
39187
|
+
try {
|
|
39188
|
+
fs4.unlinkSync(tmpPath);
|
|
39189
|
+
} catch {
|
|
39190
|
+
}
|
|
39191
|
+
throw err;
|
|
39192
|
+
}
|
|
39193
|
+
debug(MODULE15, "State saved");
|
|
39194
|
+
}
|
|
38813
39195
|
|
|
38814
39196
|
// ../core/dist/scout.js
|
|
38815
39197
|
function offlineModeMessage(reason) {
|
|
@@ -38864,8 +39246,12 @@ function toGistOctokit(octokit) {
|
|
|
38864
39246
|
};
|
|
38865
39247
|
}
|
|
38866
39248
|
async function createScout(config2) {
|
|
39249
|
+
if (config2.logLevel !== void 0) {
|
|
39250
|
+
setLogLevel(config2.logLevel);
|
|
39251
|
+
}
|
|
38867
39252
|
let state;
|
|
38868
39253
|
let gistStore = null;
|
|
39254
|
+
let persistLocal = false;
|
|
38869
39255
|
if (config2.persistence === "provided") {
|
|
38870
39256
|
state = config2.initialState;
|
|
38871
39257
|
} else if (config2.persistence === "gist") {
|
|
@@ -38882,37 +39268,61 @@ async function createScout(config2) {
|
|
|
38882
39268
|
state.gistId = result.gistId;
|
|
38883
39269
|
}
|
|
38884
39270
|
} else {
|
|
38885
|
-
state =
|
|
39271
|
+
state = loadLocalState();
|
|
39272
|
+
persistLocal = true;
|
|
38886
39273
|
}
|
|
38887
|
-
return new OssScout(config2.githubToken, state, gistStore);
|
|
39274
|
+
return new OssScout(config2.githubToken, state, gistStore, { persistLocal });
|
|
38888
39275
|
}
|
|
38889
39276
|
var OssScout = class {
|
|
38890
39277
|
githubToken;
|
|
38891
39278
|
gistStore;
|
|
38892
39279
|
state;
|
|
38893
39280
|
dirty = false;
|
|
38894
|
-
|
|
39281
|
+
/** When true, checkpoint() also writes ~/.oss-scout/state.json. */
|
|
39282
|
+
persistLocal;
|
|
39283
|
+
constructor(githubToken, initialState, gistStore = null, opts = {}) {
|
|
38895
39284
|
this.githubToken = githubToken;
|
|
38896
39285
|
this.gistStore = gistStore;
|
|
38897
39286
|
this.state = initialState;
|
|
39287
|
+
this.persistLocal = opts.persistLocal ?? false;
|
|
38898
39288
|
}
|
|
38899
39289
|
// ── Search ──────────────────────────────────────────────────────────
|
|
39290
|
+
/**
|
|
39291
|
+
* Drop stale disk-cache entries. Called at the top of every cache-burning
|
|
39292
|
+
* entry point (search, features, vetList); without it ~/.oss-scout/cache
|
|
39293
|
+
* grows without bound. evictStale never throws (fs errors degrade to warn).
|
|
39294
|
+
*/
|
|
39295
|
+
evictStaleCacheEntries() {
|
|
39296
|
+
getHttpCache().evictStale();
|
|
39297
|
+
}
|
|
38900
39298
|
/**
|
|
38901
39299
|
* Multi-strategy issue search. Returns scored, sorted candidates.
|
|
38902
39300
|
* Automatically culls expired skip entries and filters skipped issues.
|
|
38903
39301
|
*/
|
|
38904
39302
|
async search(options) {
|
|
39303
|
+
this.evictStaleCacheEntries();
|
|
38905
39304
|
this.cullExpiredSkips();
|
|
38906
39305
|
const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
38907
39306
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
39307
|
+
const prefs = this.state.preferences;
|
|
39308
|
+
const prefLangs = prefs.preferLanguages ?? [];
|
|
39309
|
+
const prefRepos = prefs.preferRepos ?? [];
|
|
39310
|
+
const preferLanguages = options?.preferLanguages ?? (prefLangs.length > 0 ? prefLangs : void 0);
|
|
39311
|
+
const preferRepos = options?.preferRepos ?? (prefRepos.length > 0 ? prefRepos : void 0);
|
|
39312
|
+
const diversityRatio = options?.diversityRatio ?? prefs.diversityRatio ?? 0;
|
|
38908
39313
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
38909
39314
|
maxResults: options?.maxResults,
|
|
38910
39315
|
strategies: options?.strategies,
|
|
38911
39316
|
skippedUrls,
|
|
38912
|
-
preferLanguages
|
|
38913
|
-
preferRepos
|
|
38914
|
-
diversityRatio
|
|
39317
|
+
preferLanguages,
|
|
39318
|
+
preferRepos,
|
|
39319
|
+
diversityRatio,
|
|
39320
|
+
interPhaseDelayMs: options?.interPhaseDelayMs,
|
|
39321
|
+
broadPhaseDelayMs: options?.broadPhaseDelayMs
|
|
38915
39322
|
});
|
|
39323
|
+
for (const c of candidates) {
|
|
39324
|
+
this.updateRepoSignalsFromHealth(c.projectHealth);
|
|
39325
|
+
}
|
|
38916
39326
|
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38917
39327
|
this.dirty = true;
|
|
38918
39328
|
return {
|
|
@@ -38923,6 +39333,28 @@ var OssScout = class {
|
|
|
38923
39333
|
strategiesUsed
|
|
38924
39334
|
};
|
|
38925
39335
|
}
|
|
39336
|
+
/**
|
|
39337
|
+
* Populate the `hasActiveMaintainers` repo-score signal from a freshly
|
|
39338
|
+
* computed projectHealth (#167). It was initialized false and never set, so
|
|
39339
|
+
* calculateScore's +1 active-maintainers weight was inert; `isActive`
|
|
39340
|
+
* (recent commit activity) is a real, already-computed proxy.
|
|
39341
|
+
*
|
|
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.
|
|
39350
|
+
*/
|
|
39351
|
+
updateRepoSignalsFromHealth(health) {
|
|
39352
|
+
if (health.checkFailed)
|
|
39353
|
+
return;
|
|
39354
|
+
this.updateRepoScore(health.repo, {
|
|
39355
|
+
signals: { hasActiveMaintainers: health.isActive }
|
|
39356
|
+
});
|
|
39357
|
+
}
|
|
38926
39358
|
/**
|
|
38927
39359
|
* Vet a single issue URL for claimability.
|
|
38928
39360
|
*/
|
|
@@ -38945,6 +39377,7 @@ var OssScout = class {
|
|
|
38945
39377
|
* preferences and excluded repos/orgs.
|
|
38946
39378
|
*/
|
|
38947
39379
|
async features(options) {
|
|
39380
|
+
this.evictStaleCacheEntries();
|
|
38948
39381
|
const count = options?.count ?? 10;
|
|
38949
39382
|
const octokit = getOctokit(this.githubToken);
|
|
38950
39383
|
const vetter = new IssueVetter(octokit, this);
|
|
@@ -38976,6 +39409,7 @@ var OssScout = class {
|
|
|
38976
39409
|
* Optionally prunes unavailable issues from saved results.
|
|
38977
39410
|
*/
|
|
38978
39411
|
async vetList(options) {
|
|
39412
|
+
this.evictStaleCacheEntries();
|
|
38979
39413
|
const saved = this.getSavedResults();
|
|
38980
39414
|
const concurrency = options?.concurrency ?? 5;
|
|
38981
39415
|
const results = [];
|
|
@@ -38991,6 +39425,7 @@ var OssScout = class {
|
|
|
38991
39425
|
number: item.number,
|
|
38992
39426
|
title: item.title,
|
|
38993
39427
|
status: this.classifyVetResult(candidate),
|
|
39428
|
+
ok: true,
|
|
38994
39429
|
recommendation: candidate.recommendation,
|
|
38995
39430
|
viabilityScore: candidate.viabilityScore
|
|
38996
39431
|
});
|
|
@@ -39007,6 +39442,7 @@ var OssScout = class {
|
|
|
39007
39442
|
number: item.number,
|
|
39008
39443
|
title: item.title,
|
|
39009
39444
|
status: isGone ? "closed" : "error",
|
|
39445
|
+
ok: false,
|
|
39010
39446
|
errorMessage: errorMessage(error48)
|
|
39011
39447
|
});
|
|
39012
39448
|
}).finally(() => {
|
|
@@ -39032,17 +39468,41 @@ var OssScout = class {
|
|
|
39032
39468
|
hasPR: results.filter((r) => r.status === "has_pr").length,
|
|
39033
39469
|
errors: results.filter((r) => r.status === "error").length
|
|
39034
39470
|
};
|
|
39471
|
+
const prevStatus = new Map((this.state.savedResults ?? []).map((r) => [r.issueUrl, r.lastStatus]));
|
|
39472
|
+
const transitions = results.filter((r) => r.status !== "error").filter((r) => {
|
|
39473
|
+
const prev = prevStatus.get(r.issueUrl);
|
|
39474
|
+
return prev !== void 0 && prev !== r.status;
|
|
39475
|
+
}).map((r) => ({
|
|
39476
|
+
issueUrl: r.issueUrl,
|
|
39477
|
+
repo: r.repo,
|
|
39478
|
+
number: r.number,
|
|
39479
|
+
from: prevStatus.get(r.issueUrl),
|
|
39480
|
+
to: r.status
|
|
39481
|
+
}));
|
|
39482
|
+
const currentStatus = new Map(results.map((r) => [r.issueUrl, r.status]));
|
|
39483
|
+
for (const saved2 of this.state.savedResults ?? []) {
|
|
39484
|
+
const status = currentStatus.get(saved2.issueUrl);
|
|
39485
|
+
if (status !== void 0 && status !== "error") {
|
|
39486
|
+
saved2.lastStatus = status;
|
|
39487
|
+
}
|
|
39488
|
+
}
|
|
39489
|
+
if (results.length > 0)
|
|
39490
|
+
this.dirty = true;
|
|
39035
39491
|
let prunedCount;
|
|
39036
39492
|
if (options?.prune) {
|
|
39037
39493
|
const unavailableUrls = new Set(results.filter((r) => r.status !== "still_available").map((r) => r.issueUrl));
|
|
39038
39494
|
const before = (this.state.savedResults ?? []).length;
|
|
39039
39495
|
this.state.savedResults = (this.state.savedResults ?? []).filter((r) => !unavailableUrls.has(r.issueUrl));
|
|
39040
39496
|
prunedCount = before - (this.state.savedResults?.length ?? 0);
|
|
39497
|
+
if (prunedCount > 0)
|
|
39498
|
+
this.addTombstones([...unavailableUrls]);
|
|
39041
39499
|
this.dirty = true;
|
|
39042
39500
|
}
|
|
39043
|
-
return { results, summary, prunedCount };
|
|
39501
|
+
return { results, summary, prunedCount, transitions };
|
|
39044
39502
|
}
|
|
39045
39503
|
classifyVetResult(candidate) {
|
|
39504
|
+
if (candidate.issueState === "closed")
|
|
39505
|
+
return "closed";
|
|
39046
39506
|
const checks = candidate.vettingResult.checks;
|
|
39047
39507
|
if (!checks.noExistingPR)
|
|
39048
39508
|
return "has_pr";
|
|
@@ -39077,19 +39537,35 @@ var OssScout = class {
|
|
|
39077
39537
|
getProjectCategories() {
|
|
39078
39538
|
return this.state.preferences.projectCategories;
|
|
39079
39539
|
}
|
|
39540
|
+
/** Configured GitHub username (used to classify own vs competing PRs, #166). */
|
|
39541
|
+
getGitHubUsername() {
|
|
39542
|
+
return this.state.preferences.githubUsername;
|
|
39543
|
+
}
|
|
39080
39544
|
getRepoScore(repo) {
|
|
39081
39545
|
const score = this.state.repoScores[repo];
|
|
39082
39546
|
return score ? score.score : null;
|
|
39083
39547
|
}
|
|
39084
39548
|
/**
|
|
39085
|
-
*
|
|
39086
|
-
*
|
|
39549
|
+
* Number of the user's PRs closed without merge in this repo (#125).
|
|
39550
|
+
* Prefers the tracked repo score; falls back to counting closedPRs so the
|
|
39551
|
+
* scoring penalty works even before a score record exists.
|
|
39552
|
+
*/
|
|
39553
|
+
getClosedWithoutMergeCount(repo) {
|
|
39554
|
+
const score = this.state.repoScores[repo];
|
|
39555
|
+
if (score)
|
|
39556
|
+
return score.closedWithoutMergeCount;
|
|
39557
|
+
return (this.state.closedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
|
|
39558
|
+
}
|
|
39559
|
+
/**
|
|
39560
|
+
* SLM pre-triage config read from preferences (oss-autopilot#1122). Returns
|
|
39561
|
+
* `null` when no `slmTriageModel` is configured — the vetter skips the SLM
|
|
39562
|
+
* call entirely (#158).
|
|
39087
39563
|
*/
|
|
39088
39564
|
getSLMTriageConfig() {
|
|
39089
|
-
|
|
39090
|
-
|
|
39091
|
-
|
|
39092
|
-
};
|
|
39565
|
+
const model = this.state.preferences.slmTriageModel ?? "";
|
|
39566
|
+
if (!model)
|
|
39567
|
+
return null;
|
|
39568
|
+
return { model, host: this.state.preferences.slmTriageHost ?? "" };
|
|
39093
39569
|
}
|
|
39094
39570
|
/** Get current preferences (read-only). */
|
|
39095
39571
|
getPreferences() {
|
|
@@ -39143,6 +39619,73 @@ var OssScout = class {
|
|
|
39143
39619
|
];
|
|
39144
39620
|
this.dirty = true;
|
|
39145
39621
|
}
|
|
39622
|
+
/**
|
|
39623
|
+
* Reconcile tracked open PRs against their current GitHub state (#164).
|
|
39624
|
+
*
|
|
39625
|
+
* `state.openPRs` was append-only — nothing transitioned an open PR to
|
|
39626
|
+
* merged/closed, so getReposWithOpenPRs() over-reported forever once a PR
|
|
39627
|
+
* merged. This checks each open PR, records merges/closures (which updates
|
|
39628
|
+
* the repo score), prunes resolved entries, and checkpoints. Cheaper than a
|
|
39629
|
+
* full bootstrap, so a host can call it on daily startup. Transient errors
|
|
39630
|
+
* leave the entry in place; auth/rate-limit failures propagate.
|
|
39631
|
+
*/
|
|
39632
|
+
async syncOpenPRs() {
|
|
39633
|
+
const octokit = getOctokit(this.githubToken);
|
|
39634
|
+
const open = this.state.openPRs ?? [];
|
|
39635
|
+
const result = {
|
|
39636
|
+
checked: open.length,
|
|
39637
|
+
merged: 0,
|
|
39638
|
+
closed: 0,
|
|
39639
|
+
stillOpen: 0,
|
|
39640
|
+
errors: 0
|
|
39641
|
+
};
|
|
39642
|
+
const remaining = [];
|
|
39643
|
+
for (const pr of open) {
|
|
39644
|
+
const parsed = parseGitHubUrl(pr.url);
|
|
39645
|
+
if (!parsed || parsed.type !== "pull") {
|
|
39646
|
+
remaining.push(pr);
|
|
39647
|
+
result.errors++;
|
|
39648
|
+
continue;
|
|
39649
|
+
}
|
|
39650
|
+
const repoFullName = `${parsed.owner}/${parsed.repo}`;
|
|
39651
|
+
try {
|
|
39652
|
+
const { data } = await octokit.pulls.get({
|
|
39653
|
+
owner: parsed.owner,
|
|
39654
|
+
repo: parsed.repo,
|
|
39655
|
+
pull_number: parsed.number
|
|
39656
|
+
});
|
|
39657
|
+
if (data.merged) {
|
|
39658
|
+
this.recordMergedPR({
|
|
39659
|
+
url: pr.url,
|
|
39660
|
+
title: pr.title,
|
|
39661
|
+
mergedAt: data.merged_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39662
|
+
repo: repoFullName
|
|
39663
|
+
});
|
|
39664
|
+
result.merged++;
|
|
39665
|
+
} else if (data.state === "closed") {
|
|
39666
|
+
this.recordClosedPR({
|
|
39667
|
+
url: pr.url,
|
|
39668
|
+
title: pr.title,
|
|
39669
|
+
closedAt: data.closed_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39670
|
+
repo: repoFullName
|
|
39671
|
+
});
|
|
39672
|
+
result.closed++;
|
|
39673
|
+
} else {
|
|
39674
|
+
remaining.push(pr);
|
|
39675
|
+
result.stillOpen++;
|
|
39676
|
+
}
|
|
39677
|
+
} catch (err) {
|
|
39678
|
+
rethrowIfFatal(err);
|
|
39679
|
+
warn("scout", `sync: could not check ${pr.url}: ${errorMessage(err)}`);
|
|
39680
|
+
remaining.push(pr);
|
|
39681
|
+
result.errors++;
|
|
39682
|
+
}
|
|
39683
|
+
}
|
|
39684
|
+
this.state.openPRs = remaining;
|
|
39685
|
+
this.dirty = true;
|
|
39686
|
+
await this.checkpoint();
|
|
39687
|
+
return result;
|
|
39688
|
+
}
|
|
39146
39689
|
/**
|
|
39147
39690
|
* Update repo score with observed signals.
|
|
39148
39691
|
*/
|
|
@@ -39177,6 +39720,7 @@ var OssScout = class {
|
|
|
39177
39720
|
*/
|
|
39178
39721
|
updatePreferences(updates) {
|
|
39179
39722
|
this.state.preferences = { ...this.state.preferences, ...updates };
|
|
39723
|
+
this.state.preferencesUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
39180
39724
|
this.dirty = true;
|
|
39181
39725
|
}
|
|
39182
39726
|
/**
|
|
@@ -39226,9 +39770,25 @@ var OssScout = class {
|
|
|
39226
39770
|
* Clear all saved results.
|
|
39227
39771
|
*/
|
|
39228
39772
|
clearResults() {
|
|
39773
|
+
this.addTombstones((this.state.savedResults ?? []).map((r) => r.issueUrl));
|
|
39229
39774
|
this.state.savedResults = [];
|
|
39230
39775
|
this.dirty = true;
|
|
39231
39776
|
}
|
|
39777
|
+
/**
|
|
39778
|
+
* Record deletion tombstones (#117) so a later gist merge does not
|
|
39779
|
+
* resurrect these URLs from another machine's copy. A re-add with a newer
|
|
39780
|
+
* timestamp overrides the tombstone in mergeStates.
|
|
39781
|
+
*/
|
|
39782
|
+
addTombstones(urls) {
|
|
39783
|
+
if (urls.length === 0)
|
|
39784
|
+
return;
|
|
39785
|
+
const removedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
39786
|
+
const existing = this.state.tombstones ?? [];
|
|
39787
|
+
const byUrl = new Map(existing.map((t) => [t.url, t]));
|
|
39788
|
+
for (const url2 of urls)
|
|
39789
|
+
byUrl.set(url2, { url: url2, removedAt });
|
|
39790
|
+
this.state.tombstones = [...byUrl.values()];
|
|
39791
|
+
}
|
|
39232
39792
|
// ── Skip List ───────────────────────────────────────────────────────
|
|
39233
39793
|
/**
|
|
39234
39794
|
* Skip an issue — excludes it from future searches. Auto-culled after 90 days.
|
|
@@ -39262,13 +39822,17 @@ var OssScout = class {
|
|
|
39262
39822
|
* Remove a specific issue from the skip list.
|
|
39263
39823
|
*/
|
|
39264
39824
|
unskipIssue(url2) {
|
|
39825
|
+
const before = (this.state.skippedIssues ?? []).length;
|
|
39265
39826
|
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url2);
|
|
39827
|
+
if (this.state.skippedIssues.length < before)
|
|
39828
|
+
this.addTombstones([url2]);
|
|
39266
39829
|
this.dirty = true;
|
|
39267
39830
|
}
|
|
39268
39831
|
/**
|
|
39269
39832
|
* Clear all skipped issues.
|
|
39270
39833
|
*/
|
|
39271
39834
|
clearSkippedIssues() {
|
|
39835
|
+
this.addTombstones((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
39272
39836
|
this.state.skippedIssues = [];
|
|
39273
39837
|
this.dirty = true;
|
|
39274
39838
|
}
|
|
@@ -39312,6 +39876,13 @@ var OssScout = class {
|
|
|
39312
39876
|
if (!ok)
|
|
39313
39877
|
return false;
|
|
39314
39878
|
}
|
|
39879
|
+
if (this.persistLocal) {
|
|
39880
|
+
try {
|
|
39881
|
+
saveLocalState(this.state);
|
|
39882
|
+
} catch {
|
|
39883
|
+
return false;
|
|
39884
|
+
}
|
|
39885
|
+
}
|
|
39315
39886
|
this.dirty = false;
|
|
39316
39887
|
return true;
|
|
39317
39888
|
}
|
|
@@ -39350,8 +39921,152 @@ var OssScout = class {
|
|
|
39350
39921
|
}
|
|
39351
39922
|
};
|
|
39352
39923
|
|
|
39924
|
+
// ../core/dist/core/preference-fields.js
|
|
39925
|
+
var FIELD_CONFIGS = {
|
|
39926
|
+
githubUsername: { type: "string" },
|
|
39927
|
+
languages: { type: "array" },
|
|
39928
|
+
labels: { type: "array" },
|
|
39929
|
+
scope: { type: "enum-array", validValues: IssueScopeSchema.options },
|
|
39930
|
+
excludeRepos: { type: "array" },
|
|
39931
|
+
excludeOrgs: { type: "array" },
|
|
39932
|
+
aiPolicyBlocklist: { type: "array" },
|
|
39933
|
+
projectCategories: {
|
|
39934
|
+
type: "enum-array",
|
|
39935
|
+
validValues: ProjectCategorySchema.options
|
|
39936
|
+
},
|
|
39937
|
+
minStars: { type: "number" },
|
|
39938
|
+
maxIssueAgeDays: { type: "number" },
|
|
39939
|
+
includeDocIssues: { type: "boolean" },
|
|
39940
|
+
minRepoScoreThreshold: { type: "number" },
|
|
39941
|
+
interPhaseDelayMs: { type: "number" },
|
|
39942
|
+
persistence: { type: "enum", validValues: PersistenceModeSchema.options },
|
|
39943
|
+
defaultStrategy: {
|
|
39944
|
+
type: "enum-array",
|
|
39945
|
+
validValues: SearchStrategySchema.options
|
|
39946
|
+
},
|
|
39947
|
+
broadPhaseDelayMs: { type: "number" },
|
|
39948
|
+
skipBroadWhenSufficientResults: { type: "number" },
|
|
39949
|
+
preferLanguages: { type: "array" },
|
|
39950
|
+
preferRepos: { type: "array" },
|
|
39951
|
+
diversityRatio: { type: "float" },
|
|
39952
|
+
slmTriageModel: { type: "string" },
|
|
39953
|
+
slmTriageHost: { type: "string" },
|
|
39954
|
+
featuresAnchorThreshold: { type: "number" },
|
|
39955
|
+
featuresSplitRatio: { type: "float" }
|
|
39956
|
+
};
|
|
39957
|
+
var PREFERENCE_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
39958
|
+
var SORTED_PREFERENCE_KEYS = [
|
|
39959
|
+
...PREFERENCE_KEYS
|
|
39960
|
+
].sort();
|
|
39961
|
+
function parseBoolean(value) {
|
|
39962
|
+
const lower = value.toLowerCase();
|
|
39963
|
+
if (lower === "true" || lower === "yes")
|
|
39964
|
+
return true;
|
|
39965
|
+
if (lower === "false" || lower === "no")
|
|
39966
|
+
return false;
|
|
39967
|
+
throw new ValidationError(`Invalid boolean value: "${value}". Use true/false or yes/no.`);
|
|
39968
|
+
}
|
|
39969
|
+
function parseIntValue(value, key) {
|
|
39970
|
+
const num = parseInt(value, 10);
|
|
39971
|
+
if (isNaN(num)) {
|
|
39972
|
+
throw new ValidationError(`Invalid number for "${key}": "${value}"`);
|
|
39973
|
+
}
|
|
39974
|
+
return num;
|
|
39975
|
+
}
|
|
39976
|
+
function parseFloatValue(value, key) {
|
|
39977
|
+
const num = Number.parseFloat(value);
|
|
39978
|
+
if (isNaN(num)) {
|
|
39979
|
+
throw new ValidationError(`Invalid number for "${key}": "${value}"`);
|
|
39980
|
+
}
|
|
39981
|
+
return num;
|
|
39982
|
+
}
|
|
39983
|
+
function parseArrayValue(value) {
|
|
39984
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
39985
|
+
}
|
|
39986
|
+
function updateArray(current, value) {
|
|
39987
|
+
if (value.startsWith("+")) {
|
|
39988
|
+
const toAdd = parseArrayValue(value.slice(1));
|
|
39989
|
+
const merged = [...current];
|
|
39990
|
+
for (const item of toAdd) {
|
|
39991
|
+
if (!merged.includes(item))
|
|
39992
|
+
merged.push(item);
|
|
39993
|
+
}
|
|
39994
|
+
return merged;
|
|
39995
|
+
}
|
|
39996
|
+
if (value.startsWith("-")) {
|
|
39997
|
+
const toRemove = new Set(parseArrayValue(value.slice(1)));
|
|
39998
|
+
return current.filter((item) => !toRemove.has(item));
|
|
39999
|
+
}
|
|
40000
|
+
return parseArrayValue(value);
|
|
40001
|
+
}
|
|
40002
|
+
function applyPreferenceField(preferences, key, value) {
|
|
40003
|
+
const field = FIELD_CONFIGS[key];
|
|
40004
|
+
if (!field) {
|
|
40005
|
+
throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${SORTED_PREFERENCE_KEYS.join(", ")}`);
|
|
40006
|
+
}
|
|
40007
|
+
const prefs = { ...preferences };
|
|
40008
|
+
switch (field.type) {
|
|
40009
|
+
case "string":
|
|
40010
|
+
prefs[key] = value;
|
|
40011
|
+
break;
|
|
40012
|
+
case "boolean":
|
|
40013
|
+
prefs[key] = parseBoolean(value);
|
|
40014
|
+
break;
|
|
40015
|
+
case "number":
|
|
40016
|
+
prefs[key] = parseIntValue(value, key);
|
|
40017
|
+
break;
|
|
40018
|
+
case "float":
|
|
40019
|
+
prefs[key] = parseFloatValue(value, key);
|
|
40020
|
+
break;
|
|
40021
|
+
case "array": {
|
|
40022
|
+
const current = prefs[key] ?? [];
|
|
40023
|
+
prefs[key] = updateArray(current, value);
|
|
40024
|
+
break;
|
|
40025
|
+
}
|
|
40026
|
+
case "enum": {
|
|
40027
|
+
const validValues = field.validValues;
|
|
40028
|
+
if (!validValues.includes(value)) {
|
|
40029
|
+
throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(", ")}`);
|
|
40030
|
+
}
|
|
40031
|
+
prefs[key] = value;
|
|
40032
|
+
break;
|
|
40033
|
+
}
|
|
40034
|
+
case "enum-array": {
|
|
40035
|
+
const current = prefs[key] ?? [];
|
|
40036
|
+
const updated = updateArray(current, value);
|
|
40037
|
+
const validValues = field.validValues;
|
|
40038
|
+
const invalid = updated.filter((s) => !validValues.includes(s));
|
|
40039
|
+
if (invalid.length > 0) {
|
|
40040
|
+
throw new ValidationError(`Invalid value(s) for "${key}": ${invalid.join(", ")}. Valid: ${validValues.join(", ")}`);
|
|
40041
|
+
}
|
|
40042
|
+
if (key === "scope") {
|
|
40043
|
+
prefs[key] = updated.length > 0 ? updated : void 0;
|
|
40044
|
+
} else {
|
|
40045
|
+
prefs[key] = updated;
|
|
40046
|
+
}
|
|
40047
|
+
break;
|
|
40048
|
+
}
|
|
40049
|
+
}
|
|
40050
|
+
return ScoutPreferencesSchema.parse(prefs);
|
|
40051
|
+
}
|
|
40052
|
+
|
|
40053
|
+
// ../core/dist/commands/validation.js
|
|
40054
|
+
var ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
|
|
40055
|
+
var MAX_URL_LENGTH = 2048;
|
|
40056
|
+
function validateGitHubUrl(url2, pattern, entityType) {
|
|
40057
|
+
if (pattern.test(url2))
|
|
40058
|
+
return;
|
|
40059
|
+
throw new ValidationError(`Invalid ${entityType} URL: ${url2}. Expected format: https://github.com/owner/repo/issues/123`);
|
|
40060
|
+
}
|
|
40061
|
+
function validateUrl(url2) {
|
|
40062
|
+
if (url2.length > MAX_URL_LENGTH) {
|
|
40063
|
+
throw new ValidationError(`URL exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
40064
|
+
}
|
|
40065
|
+
return url2;
|
|
40066
|
+
}
|
|
40067
|
+
|
|
39353
40068
|
// src/tools.ts
|
|
39354
|
-
var TOOL_TIMEOUT_MS =
|
|
40069
|
+
var TOOL_TIMEOUT_MS = 3e5;
|
|
39355
40070
|
function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
|
|
39356
40071
|
let timeoutId;
|
|
39357
40072
|
const timer = new Promise((_, reject) => {
|
|
@@ -39396,7 +40111,11 @@ function registerTools(server, scout) {
|
|
|
39396
40111
|
strategies: parsedStrategies,
|
|
39397
40112
|
preferLanguages,
|
|
39398
40113
|
preferRepos,
|
|
39399
|
-
diversityRatio
|
|
40114
|
+
diversityRatio,
|
|
40115
|
+
// No fixed inter-phase sleeps in the request/response MCP context;
|
|
40116
|
+
// the budget tracker still paces the GitHub calls (#143)
|
|
40117
|
+
interPhaseDelayMs: 0,
|
|
40118
|
+
broadPhaseDelayMs: 0
|
|
39400
40119
|
})
|
|
39401
40120
|
);
|
|
39402
40121
|
scout.saveResults(result.candidates);
|
|
@@ -39477,6 +40196,24 @@ function registerTools(server, scout) {
|
|
|
39477
40196
|
};
|
|
39478
40197
|
}
|
|
39479
40198
|
);
|
|
40199
|
+
server.tool(
|
|
40200
|
+
"sync",
|
|
40201
|
+
"Reconcile tracked open PRs against GitHub \u2014 mark merged/closed and recompute repo scores. Cheap; good for a daily startup call.",
|
|
40202
|
+
async () => {
|
|
40203
|
+
try {
|
|
40204
|
+
const result = await withTimeout(scout.syncOpenPRs());
|
|
40205
|
+
return {
|
|
40206
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
40207
|
+
};
|
|
40208
|
+
} catch (err) {
|
|
40209
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
40210
|
+
return {
|
|
40211
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
40212
|
+
isError: true
|
|
40213
|
+
};
|
|
40214
|
+
}
|
|
40215
|
+
}
|
|
40216
|
+
);
|
|
39480
40217
|
server.tool(
|
|
39481
40218
|
"skip",
|
|
39482
40219
|
"Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.",
|
|
@@ -39507,7 +40244,7 @@ function registerTools(server, scout) {
|
|
|
39507
40244
|
const count = scout.getSkippedIssues().length;
|
|
39508
40245
|
scout.clearSkippedIssues();
|
|
39509
40246
|
const synced2 = await scout.checkpoint();
|
|
39510
|
-
const syncNote2 = synced2 ? "" : " (
|
|
40247
|
+
const syncNote2 = synced2 ? "" : " (warning: failed to persist to disk)";
|
|
39511
40248
|
return {
|
|
39512
40249
|
content: [
|
|
39513
40250
|
{
|
|
@@ -39521,7 +40258,7 @@ function registerTools(server, scout) {
|
|
|
39521
40258
|
const wasPresent = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
39522
40259
|
scout.unskipIssue(issueUrl);
|
|
39523
40260
|
const synced2 = await scout.checkpoint();
|
|
39524
|
-
const syncNote2 = synced2 ? "" : " (
|
|
40261
|
+
const syncNote2 = synced2 ? "" : " (warning: failed to persist to disk)";
|
|
39525
40262
|
return {
|
|
39526
40263
|
content: [
|
|
39527
40264
|
{
|
|
@@ -39531,6 +40268,8 @@ function registerTools(server, scout) {
|
|
|
39531
40268
|
]
|
|
39532
40269
|
};
|
|
39533
40270
|
}
|
|
40271
|
+
validateUrl(issueUrl);
|
|
40272
|
+
validateGitHubUrl(issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
39534
40273
|
const alreadySkipped = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
39535
40274
|
if (alreadySkipped) {
|
|
39536
40275
|
return {
|
|
@@ -39547,7 +40286,7 @@ function registerTools(server, scout) {
|
|
|
39547
40286
|
} : void 0;
|
|
39548
40287
|
scout.skipIssue(issueUrl, metadata);
|
|
39549
40288
|
const synced = await scout.checkpoint();
|
|
39550
|
-
const syncNote = synced ? "" : " (
|
|
40289
|
+
const syncNote = synced ? "" : " (warning: failed to persist to disk)";
|
|
39551
40290
|
return {
|
|
39552
40291
|
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }]
|
|
39553
40292
|
};
|
|
@@ -39560,101 +40299,31 @@ function registerTools(server, scout) {
|
|
|
39560
40299
|
}
|
|
39561
40300
|
}
|
|
39562
40301
|
);
|
|
39563
|
-
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
39564
|
-
const ARRAY_KEYS = /* @__PURE__ */ new Set([
|
|
39565
|
-
"languages",
|
|
39566
|
-
"labels",
|
|
39567
|
-
"projectCategories",
|
|
39568
|
-
"excludeRepos",
|
|
39569
|
-
"excludeOrgs",
|
|
39570
|
-
"aiPolicyBlocklist",
|
|
39571
|
-
"scope",
|
|
39572
|
-
"defaultStrategy"
|
|
39573
|
-
]);
|
|
39574
|
-
const NUMBER_KEYS = /* @__PURE__ */ new Set([
|
|
39575
|
-
"minStars",
|
|
39576
|
-
"maxIssueAgeDays",
|
|
39577
|
-
"minRepoScoreThreshold",
|
|
39578
|
-
"interPhaseDelayMs",
|
|
39579
|
-
"broadPhaseDelayMs",
|
|
39580
|
-
"skipBroadWhenSufficientResults",
|
|
39581
|
-
"featuresAnchorThreshold",
|
|
39582
|
-
"featuresSplitRatio"
|
|
39583
|
-
]);
|
|
39584
|
-
const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
|
|
39585
40302
|
server.tool(
|
|
39586
40303
|
"config-set",
|
|
39587
40304
|
"Update an oss-scout configuration preference",
|
|
39588
40305
|
{
|
|
39589
40306
|
key: external_exports3.string().describe(
|
|
39590
|
-
|
|
40307
|
+
`Preference key to update. One of: ${SORTED_PREFERENCE_KEYS.join(", ")}`
|
|
39591
40308
|
),
|
|
39592
40309
|
value: external_exports3.string().describe(
|
|
39593
|
-
|
|
40310
|
+
'New value. Comma-separated for arrays; prefix with + to append or - to remove items (e.g. "+spam/repo"). Plain string/number/boolean for scalars.'
|
|
39594
40311
|
)
|
|
39595
40312
|
},
|
|
39596
40313
|
async ({ key, value }) => {
|
|
39597
|
-
|
|
39598
|
-
|
|
39599
|
-
|
|
39600
|
-
|
|
39601
|
-
|
|
39602
|
-
text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`
|
|
39603
|
-
}
|
|
39604
|
-
],
|
|
39605
|
-
isError: true
|
|
39606
|
-
};
|
|
39607
|
-
}
|
|
39608
|
-
let parsed;
|
|
39609
|
-
if (ARRAY_KEYS.has(key)) {
|
|
39610
|
-
try {
|
|
39611
|
-
parsed = JSON.parse(value);
|
|
39612
|
-
if (!Array.isArray(parsed)) {
|
|
39613
|
-
parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39614
|
-
}
|
|
39615
|
-
} catch {
|
|
39616
|
-
parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39617
|
-
}
|
|
39618
|
-
} else if (NUMBER_KEYS.has(key)) {
|
|
39619
|
-
const num = Number(value);
|
|
39620
|
-
if (isNaN(num)) {
|
|
39621
|
-
return {
|
|
39622
|
-
content: [
|
|
39623
|
-
{ type: "text", text: `Invalid number for "${key}": "${value}"` }
|
|
39624
|
-
],
|
|
39625
|
-
isError: true
|
|
39626
|
-
};
|
|
39627
|
-
}
|
|
39628
|
-
parsed = num;
|
|
39629
|
-
} else if (BOOLEAN_KEYS.has(key)) {
|
|
39630
|
-
const lower = value.toLowerCase();
|
|
39631
|
-
if (lower === "true" || lower === "yes") parsed = true;
|
|
39632
|
-
else if (lower === "false" || lower === "no") parsed = false;
|
|
39633
|
-
else {
|
|
39634
|
-
return {
|
|
39635
|
-
content: [
|
|
39636
|
-
{
|
|
39637
|
-
type: "text",
|
|
39638
|
-
text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`
|
|
39639
|
-
}
|
|
39640
|
-
],
|
|
39641
|
-
isError: true
|
|
39642
|
-
};
|
|
39643
|
-
}
|
|
39644
|
-
} else {
|
|
39645
|
-
parsed = value;
|
|
39646
|
-
}
|
|
39647
|
-
const currentPrefs = scout.getPreferences();
|
|
39648
|
-
const candidate = { ...currentPrefs, [key]: parsed };
|
|
39649
|
-
const result = ScoutPreferencesSchema.safeParse(candidate);
|
|
39650
|
-
if (!result.success) {
|
|
39651
|
-
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
40314
|
+
let validated;
|
|
40315
|
+
try {
|
|
40316
|
+
validated = applyPreferenceField(scout.getPreferences(), key, value);
|
|
40317
|
+
} catch (err) {
|
|
40318
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39652
40319
|
return {
|
|
39653
|
-
content: [{ type: "text", text:
|
|
40320
|
+
content: [{ type: "text", text: msg }],
|
|
39654
40321
|
isError: true
|
|
39655
40322
|
};
|
|
39656
40323
|
}
|
|
39657
|
-
scout.updatePreferences({
|
|
40324
|
+
scout.updatePreferences({
|
|
40325
|
+
[key]: validated[key]
|
|
40326
|
+
});
|
|
39658
40327
|
await scout.checkpoint();
|
|
39659
40328
|
const updated = scout.getPreferences();
|
|
39660
40329
|
return {
|
|
@@ -39714,25 +40383,34 @@ function registerResources(server, scout) {
|
|
|
39714
40383
|
}
|
|
39715
40384
|
|
|
39716
40385
|
// src/index.ts
|
|
39717
|
-
|
|
39718
|
-
|
|
39719
|
-
|
|
39720
|
-
|
|
39721
|
-
|
|
39722
|
-
|
|
39723
|
-
process.exit(1);
|
|
39724
|
-
}
|
|
39725
|
-
const scout = await createScout({ githubToken: token });
|
|
40386
|
+
var SERVER_INFO = {
|
|
40387
|
+
name: "oss-scout-mcp",
|
|
40388
|
+
version: "0.9.0"
|
|
40389
|
+
// x-release-please-version
|
|
40390
|
+
};
|
|
40391
|
+
function createServer(scout) {
|
|
39726
40392
|
const server = new McpServer(
|
|
39727
|
-
{ name:
|
|
40393
|
+
{ name: SERVER_INFO.name, version: SERVER_INFO.version },
|
|
39728
40394
|
{ capabilities: { resources: {}, tools: {} } }
|
|
39729
40395
|
);
|
|
39730
40396
|
registerTools(server, scout);
|
|
39731
40397
|
registerResources(server, scout);
|
|
39732
|
-
|
|
39733
|
-
await server.connect(transport);
|
|
40398
|
+
return server;
|
|
39734
40399
|
}
|
|
39735
|
-
|
|
40400
|
+
async function runServer() {
|
|
40401
|
+
const token = getGitHubToken();
|
|
40402
|
+
if (!token) {
|
|
40403
|
+
throw new Error(
|
|
40404
|
+
"GitHub token required. Set GITHUB_TOKEN or run `gh auth login`."
|
|
40405
|
+
);
|
|
40406
|
+
}
|
|
40407
|
+
const scout = await createScout({ githubToken: token, persistence: "local" });
|
|
40408
|
+
const server = createServer(scout);
|
|
40409
|
+
await server.connect(new StdioServerTransport());
|
|
40410
|
+
}
|
|
40411
|
+
|
|
40412
|
+
// src/bin.ts
|
|
40413
|
+
runServer().catch((err) => {
|
|
39736
40414
|
process.stderr.write(
|
|
39737
40415
|
`oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
|
|
39738
40416
|
`
|