@oss-scout/mcp 0.7.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 +1306 -577
- package/dist/tools.js +59 -103
- 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,32 +37903,72 @@ 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
|
-
|
|
37926
|
+
if (score === 0)
|
|
37927
|
+
return c;
|
|
37928
|
+
return { ...c, personalization: { kind: "boosted", score, reasons } };
|
|
37929
|
+
});
|
|
37930
|
+
}
|
|
37931
|
+
function applyDiversityRatio(candidates, maxResults, diversityRatio) {
|
|
37932
|
+
if (maxResults <= 0)
|
|
37933
|
+
return [];
|
|
37934
|
+
const ratio = Math.max(0, Math.min(1, diversityRatio));
|
|
37935
|
+
if (ratio === 0)
|
|
37936
|
+
return candidates.slice(0, maxResults);
|
|
37937
|
+
const diversityReserve = Math.min(Math.floor(maxResults * ratio), maxResults);
|
|
37938
|
+
if (diversityReserve === 0)
|
|
37939
|
+
return candidates.slice(0, maxResults);
|
|
37940
|
+
const mainBudget = maxResults - diversityReserve;
|
|
37941
|
+
const picks = [];
|
|
37942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
37943
|
+
for (const c of candidates) {
|
|
37944
|
+
if (picks.length >= mainBudget)
|
|
37945
|
+
break;
|
|
37946
|
+
picks.push(c);
|
|
37947
|
+
seen.add(c.issue.url);
|
|
37948
|
+
}
|
|
37949
|
+
for (const c of candidates) {
|
|
37950
|
+
if (picks.length >= maxResults)
|
|
37951
|
+
break;
|
|
37952
|
+
if (seen.has(c.issue.url))
|
|
37953
|
+
continue;
|
|
37954
|
+
if (boostScoreOf(c) > 0)
|
|
37955
|
+
continue;
|
|
37956
|
+
picks.push({ ...c, personalization: { kind: "diversity" } });
|
|
37957
|
+
seen.add(c.issue.url);
|
|
37627
37958
|
}
|
|
37959
|
+
for (const c of candidates) {
|
|
37960
|
+
if (picks.length >= maxResults)
|
|
37961
|
+
break;
|
|
37962
|
+
if (seen.has(c.issue.url))
|
|
37963
|
+
continue;
|
|
37964
|
+
picks.push(c);
|
|
37965
|
+
seen.add(c.issue.url);
|
|
37966
|
+
}
|
|
37967
|
+
return picks;
|
|
37628
37968
|
}
|
|
37629
37969
|
|
|
37630
37970
|
// ../core/dist/core/issue-discovery.js
|
|
37631
|
-
var
|
|
37971
|
+
var MODULE11 = "issue-discovery";
|
|
37632
37972
|
var LOW_BUDGET_THRESHOLD = 20;
|
|
37633
37973
|
var CRITICAL_BUDGET_THRESHOLD = 10;
|
|
37634
37974
|
function buildIssueFilter(config2) {
|
|
@@ -37637,16 +37977,17 @@ function buildIssueFilter(config2) {
|
|
|
37637
37977
|
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
37638
37978
|
if (!repoFullName)
|
|
37639
37979
|
return false;
|
|
37640
|
-
|
|
37980
|
+
const repoLower = repoFullName.toLowerCase();
|
|
37981
|
+
if (config2.excludedRepos.has(repoLower))
|
|
37641
37982
|
return false;
|
|
37642
37983
|
if (config2.excludeOrgs.size > 0) {
|
|
37643
|
-
const orgName =
|
|
37984
|
+
const orgName = repoLower.split("/")[0];
|
|
37644
37985
|
if (orgName && config2.excludeOrgs.has(orgName))
|
|
37645
37986
|
return false;
|
|
37646
37987
|
}
|
|
37647
|
-
if (config2.aiBlocklisted.has(
|
|
37988
|
+
if (config2.aiBlocklisted.has(repoLower))
|
|
37648
37989
|
return false;
|
|
37649
|
-
if (config2.lowScoringRepos.has(
|
|
37990
|
+
if (config2.lowScoringRepos.has(repoLower))
|
|
37650
37991
|
return false;
|
|
37651
37992
|
if (config2.skippedUrls.has(item.html_url))
|
|
37652
37993
|
return false;
|
|
@@ -37661,9 +38002,9 @@ function buildIssueFilter(config2) {
|
|
|
37661
38002
|
};
|
|
37662
38003
|
}
|
|
37663
38004
|
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
37664
|
-
info(
|
|
38005
|
+
info(MODULE11, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
37665
38006
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
37666
|
-
info(
|
|
38007
|
+
info(MODULE11, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
37667
38008
|
return {
|
|
37668
38009
|
candidates,
|
|
37669
38010
|
error: allReposFailed ? "All merged-PR repo fetches failed" : null,
|
|
@@ -37671,11 +38012,11 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
|
37671
38012
|
};
|
|
37672
38013
|
}
|
|
37673
38014
|
async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
|
|
37674
|
-
info(
|
|
38015
|
+
info(MODULE11, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
37675
38016
|
const phase1Labels = labels.slice(0, 3);
|
|
37676
38017
|
const reposToSearch = repos.slice(0, 10);
|
|
37677
38018
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
|
|
37678
|
-
info(
|
|
38019
|
+
info(MODULE11, `Found ${candidates.length} candidates from starred repos`);
|
|
37679
38020
|
return {
|
|
37680
38021
|
candidates,
|
|
37681
38022
|
error: allReposFailed ? "All starred repo fetches failed" : null,
|
|
@@ -37683,14 +38024,14 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
|
|
|
37683
38024
|
};
|
|
37684
38025
|
}
|
|
37685
38026
|
async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
37686
|
-
info(
|
|
38027
|
+
info(MODULE11, "Phase 2: General issue search...");
|
|
37687
38028
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37688
38029
|
const tierLabelGroups = [];
|
|
37689
38030
|
if (scopes && scopes.length > 1) {
|
|
37690
38031
|
for (const scope of scopes) {
|
|
37691
38032
|
const scopeLabels = SCOPE_LABELS[scope] ?? [];
|
|
37692
38033
|
if (scopeLabels.length === 0) {
|
|
37693
|
-
warn(
|
|
38034
|
+
warn(MODULE11, `Scope "${scope}" has no labels, skipping tier`);
|
|
37694
38035
|
continue;
|
|
37695
38036
|
}
|
|
37696
38037
|
tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
|
|
@@ -37710,7 +38051,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37710
38051
|
for (const { tier, tierLabels } of tierLabelGroups) {
|
|
37711
38052
|
try {
|
|
37712
38053
|
const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
|
|
37713
|
-
info(
|
|
38054
|
+
info(MODULE11, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
|
|
37714
38055
|
const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
|
|
37715
38056
|
tierResults.push(tierCandidates);
|
|
37716
38057
|
for (const c of tierCandidates)
|
|
@@ -37721,7 +38062,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37721
38062
|
if (vetRateLimitHit) {
|
|
37722
38063
|
rateLimitHit = true;
|
|
37723
38064
|
}
|
|
37724
|
-
info(
|
|
38065
|
+
info(MODULE11, `Found ${tierCandidates.length} candidates from ${tier} tier`);
|
|
37725
38066
|
} catch (err) {
|
|
37726
38067
|
if (getHttpStatusCode(err) === 401)
|
|
37727
38068
|
throw err;
|
|
@@ -37730,13 +38071,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37730
38071
|
if (isRateLimitError(err)) {
|
|
37731
38072
|
rateLimitHit = true;
|
|
37732
38073
|
}
|
|
37733
|
-
warn(
|
|
38074
|
+
warn(MODULE11, `Error in ${tier} tier search: ${errMsg}`);
|
|
37734
38075
|
tierResults.push([]);
|
|
37735
38076
|
}
|
|
37736
38077
|
}
|
|
37737
38078
|
const interleaved = interleaveArrays(tierResults);
|
|
37738
38079
|
if (interleaved.length === 0 && error48) {
|
|
37739
|
-
warn(
|
|
38080
|
+
warn(MODULE11, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
|
|
37740
38081
|
}
|
|
37741
38082
|
return {
|
|
37742
38083
|
candidates: interleaved.slice(0, maxResults),
|
|
@@ -37745,17 +38086,17 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37745
38086
|
};
|
|
37746
38087
|
}
|
|
37747
38088
|
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
|
|
37748
|
-
info(
|
|
38089
|
+
info(MODULE11, "Phase 3: Searching actively maintained repos...");
|
|
37749
38090
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37750
38091
|
const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
|
|
37751
38092
|
if (eligibleStarred.length > 0) {
|
|
37752
|
-
info(
|
|
38093
|
+
info(MODULE11, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
|
|
37753
38094
|
const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
|
|
37754
38095
|
if (restItems.length > 0) {
|
|
37755
38096
|
try {
|
|
37756
38097
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
|
|
37757
38098
|
if (candidates.length > 0) {
|
|
37758
|
-
info(
|
|
38099
|
+
info(MODULE11, `Found ${candidates.length} candidates from maintained-repo REST search`);
|
|
37759
38100
|
return {
|
|
37760
38101
|
candidates,
|
|
37761
38102
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37765,11 +38106,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37765
38106
|
} catch (error48) {
|
|
37766
38107
|
if (getHttpStatusCode(error48) === 401)
|
|
37767
38108
|
throw error48;
|
|
37768
|
-
warn(
|
|
38109
|
+
warn(MODULE11, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
|
|
37769
38110
|
}
|
|
37770
38111
|
}
|
|
37771
38112
|
}
|
|
37772
|
-
info(
|
|
38113
|
+
info(MODULE11, "Phase 3: Falling back to Search API...");
|
|
37773
38114
|
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
37774
38115
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
37775
38116
|
const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
|
|
@@ -37783,9 +38124,9 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37783
38124
|
order: "desc",
|
|
37784
38125
|
per_page: maxResults * 3
|
|
37785
38126
|
});
|
|
37786
|
-
info(
|
|
38127
|
+
info(MODULE11, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
37787
38128
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
|
|
37788
|
-
info(
|
|
38129
|
+
info(MODULE11, `Found ${candidates.length} candidates from maintained-repo search`);
|
|
37789
38130
|
return {
|
|
37790
38131
|
candidates,
|
|
37791
38132
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37795,7 +38136,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37795
38136
|
if (getHttpStatusCode(error48) === 401)
|
|
37796
38137
|
throw error48;
|
|
37797
38138
|
const errMsg = errorMessage(error48);
|
|
37798
|
-
warn(
|
|
38139
|
+
warn(MODULE11, `Error in maintained-repo search: ${errMsg}`);
|
|
37799
38140
|
return {
|
|
37800
38141
|
candidates: [],
|
|
37801
38142
|
error: errMsg,
|
|
@@ -37861,9 +38202,10 @@ var IssueDiscovery = class {
|
|
|
37861
38202
|
const labels = options.labels || (scopes ? buildEffectiveLabels(scopes, config2.labels) : config2.labels);
|
|
37862
38203
|
const maxResults = options.maxResults || 10;
|
|
37863
38204
|
const minStars = config2.minStars ?? 50;
|
|
37864
|
-
const interPhaseDelay = config2.interPhaseDelayMs ?? 3e4;
|
|
38205
|
+
const interPhaseDelay = options.interPhaseDelayMs ?? config2.interPhaseDelayMs ?? 3e4;
|
|
37865
38206
|
const ALL_STRATEGIES = CONCRETE_STRATEGIES;
|
|
37866
|
-
const
|
|
38207
|
+
const pickStrategies = (...candidates) => candidates.find((c) => c && c.length > 0) ?? ["all"];
|
|
38208
|
+
const rawStrategies = pickStrategies(options.strategies, config2.defaultStrategy);
|
|
37867
38209
|
const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
|
|
37868
38210
|
const strategiesUsed = [];
|
|
37869
38211
|
const allCandidates = [];
|
|
@@ -37879,18 +38221,18 @@ var IssueDiscovery = class {
|
|
|
37879
38221
|
if (rateLimit.remaining < 5) {
|
|
37880
38222
|
const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
|
|
37881
38223
|
this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
|
|
37882
|
-
warn(
|
|
38224
|
+
warn(MODULE11, this.rateLimitWarning);
|
|
37883
38225
|
}
|
|
37884
38226
|
if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
|
|
37885
|
-
info(
|
|
38227
|
+
info(MODULE11, `Search budget critical (${searchBudget} remaining) \u2014 running only Phase 0`);
|
|
37886
38228
|
} else if (searchBudget < LOW_BUDGET_THRESHOLD) {
|
|
37887
|
-
info(
|
|
38229
|
+
info(MODULE11, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
|
|
37888
38230
|
}
|
|
37889
38231
|
} catch (error48) {
|
|
37890
38232
|
if (getHttpStatusCode(error48) === 401)
|
|
37891
38233
|
throw error48;
|
|
37892
38234
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
|
|
37893
|
-
warn(
|
|
38235
|
+
warn(MODULE11, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
|
|
37894
38236
|
}
|
|
37895
38237
|
if (searchBudget <= 0) {
|
|
37896
38238
|
this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
@@ -37900,15 +38242,15 @@ var IssueDiscovery = class {
|
|
|
37900
38242
|
const openPRRepos = this.stateReader.getReposWithOpenPRs();
|
|
37901
38243
|
const starredRepos = this.getStarredRepos();
|
|
37902
38244
|
const starredRepoSet = new Set(starredRepos);
|
|
37903
|
-
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
|
|
38245
|
+
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold).map((r) => r.toLowerCase()));
|
|
37904
38246
|
const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
|
|
37905
38247
|
const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
|
|
37906
|
-
const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
|
|
38248
|
+
const aiBlocklisted = new Set(config2.aiPolicyBlocklist.map((r) => r.toLowerCase()));
|
|
37907
38249
|
if (aiBlocklisted.size > 0) {
|
|
37908
|
-
debug(
|
|
38250
|
+
debug(MODULE11, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
|
|
37909
38251
|
}
|
|
37910
38252
|
const filterIssues = buildIssueFilter({
|
|
37911
|
-
excludedRepos: new Set(config2.excludeRepos),
|
|
38253
|
+
excludedRepos: new Set(config2.excludeRepos.map((r) => r.toLowerCase())),
|
|
37912
38254
|
excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
37913
38255
|
aiBlocklisted,
|
|
37914
38256
|
lowScoringRepos,
|
|
@@ -37941,7 +38283,7 @@ var IssueDiscovery = class {
|
|
|
37941
38283
|
}
|
|
37942
38284
|
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
|
|
37943
38285
|
if (interPhaseDelay > 0) {
|
|
37944
|
-
info(
|
|
38286
|
+
info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37945
38287
|
await sleep(interPhaseDelay);
|
|
37946
38288
|
}
|
|
37947
38289
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
@@ -37953,25 +38295,26 @@ var IssueDiscovery = class {
|
|
|
37953
38295
|
phaseErrors["1"] = result.error;
|
|
37954
38296
|
if (result.rateLimitHit)
|
|
37955
38297
|
rateLimitHitDuringSearch = true;
|
|
38298
|
+
strategiesUsed.push("starred");
|
|
37956
38299
|
}
|
|
37957
38300
|
}
|
|
37958
|
-
strategiesUsed.push("starred");
|
|
37959
38301
|
}
|
|
37960
|
-
const broadDelay = config2.broadPhaseDelayMs ?? 9e4;
|
|
37961
|
-
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;
|
|
37962
38305
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
|
|
37963
38306
|
if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
|
|
37964
|
-
info(
|
|
38307
|
+
info(MODULE11, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
|
|
37965
38308
|
} else {
|
|
37966
38309
|
if (interPhaseDelay > 0) {
|
|
37967
|
-
info(
|
|
38310
|
+
info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37968
38311
|
await sleep(interPhaseDelay);
|
|
37969
38312
|
}
|
|
37970
38313
|
if (allCandidates.length > 0 && broadDelay > 0) {
|
|
37971
|
-
info(
|
|
38314
|
+
info(MODULE11, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
|
|
37972
38315
|
await sleep(broadDelay);
|
|
37973
38316
|
} else if (allCandidates.length === 0) {
|
|
37974
|
-
info(
|
|
38317
|
+
info(MODULE11, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
37975
38318
|
}
|
|
37976
38319
|
const remaining = maxResults - allCandidates.length;
|
|
37977
38320
|
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
@@ -37979,12 +38322,12 @@ var IssueDiscovery = class {
|
|
|
37979
38322
|
phaseErrors["2"] = result.error;
|
|
37980
38323
|
if (result.rateLimitHit)
|
|
37981
38324
|
rateLimitHitDuringSearch = true;
|
|
38325
|
+
strategiesUsed.push("broad");
|
|
37982
38326
|
}
|
|
37983
|
-
strategiesUsed.push("broad");
|
|
37984
38327
|
}
|
|
37985
38328
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
|
|
37986
38329
|
if (interPhaseDelay > 0) {
|
|
37987
|
-
info(
|
|
38330
|
+
info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37988
38331
|
await sleep(interPhaseDelay);
|
|
37989
38332
|
}
|
|
37990
38333
|
const remaining = maxResults - allCandidates.length;
|
|
@@ -38019,8 +38362,8 @@ var IssueDiscovery = class {
|
|
|
38019
38362
|
if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
|
|
38020
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.`;
|
|
38021
38364
|
}
|
|
38022
|
-
annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
|
|
38023
|
-
|
|
38365
|
+
const ranked = annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
|
|
38366
|
+
ranked.sort((a, b) => {
|
|
38024
38367
|
const priorityOrder = {
|
|
38025
38368
|
merged_pr: 0,
|
|
38026
38369
|
starred: 1,
|
|
@@ -38033,14 +38376,15 @@ var IssueDiscovery = class {
|
|
|
38033
38376
|
const recDiff = recommendationOrder[a.recommendation] - recommendationOrder[b.recommendation];
|
|
38034
38377
|
if (recDiff !== 0)
|
|
38035
38378
|
return recDiff;
|
|
38036
|
-
const boostDiff = (b
|
|
38379
|
+
const boostDiff = boostScoreOf(b) - boostScoreOf(a);
|
|
38037
38380
|
if (boostDiff !== 0)
|
|
38038
38381
|
return boostDiff;
|
|
38039
38382
|
return b.viabilityScore - a.viabilityScore;
|
|
38040
38383
|
});
|
|
38041
|
-
const capped = applyPerRepoCap(
|
|
38042
|
-
|
|
38043
|
-
|
|
38384
|
+
const capped = applyPerRepoCap(ranked, 2);
|
|
38385
|
+
const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
|
|
38386
|
+
info(MODULE11, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${finalPicks.length} candidates returned`);
|
|
38387
|
+
return { candidates: finalPicks, strategiesUsed };
|
|
38044
38388
|
}
|
|
38045
38389
|
/**
|
|
38046
38390
|
* Vet a specific issue for claimability and project health.
|
|
@@ -38072,7 +38416,7 @@ var IssueDiscovery = class {
|
|
|
38072
38416
|
};
|
|
38073
38417
|
|
|
38074
38418
|
// ../core/dist/core/roadmap.js
|
|
38075
|
-
var
|
|
38419
|
+
var MODULE12 = "roadmap";
|
|
38076
38420
|
var CACHE_TTL_MS2 = 60 * 60 * 1e3;
|
|
38077
38421
|
var ROADMAP_PATHS = [
|
|
38078
38422
|
"ROADMAP.md",
|
|
@@ -38126,6 +38470,19 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
38126
38470
|
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS2) {
|
|
38127
38471
|
return cached2.refs;
|
|
38128
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) {
|
|
38129
38486
|
for (const path5 of ROADMAP_PATHS) {
|
|
38130
38487
|
try {
|
|
38131
38488
|
const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
|
|
@@ -38137,12 +38494,11 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
38137
38494
|
pruneCache2();
|
|
38138
38495
|
return refs;
|
|
38139
38496
|
} catch (err) {
|
|
38140
|
-
|
|
38141
|
-
throw err;
|
|
38497
|
+
rethrowIfFatal(err);
|
|
38142
38498
|
const status = getHttpStatusCode(err);
|
|
38143
38499
|
if (status === 404)
|
|
38144
38500
|
continue;
|
|
38145
|
-
warn(
|
|
38501
|
+
warn(MODULE12, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(err)}`);
|
|
38146
38502
|
}
|
|
38147
38503
|
}
|
|
38148
38504
|
const empty = /* @__PURE__ */ new Set();
|
|
@@ -38152,7 +38508,7 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
38152
38508
|
}
|
|
38153
38509
|
|
|
38154
38510
|
// ../core/dist/core/feature-discovery.js
|
|
38155
|
-
var
|
|
38511
|
+
var MODULE13 = "feature-discovery";
|
|
38156
38512
|
var INTER_REPO_DELAY_MS = 2e3;
|
|
38157
38513
|
var MIN_VIABILITY_SCORE = 40;
|
|
38158
38514
|
var ANCHOR_THRESHOLD = 3;
|
|
@@ -38244,6 +38600,37 @@ function isFeatureIssue(item) {
|
|
|
38244
38600
|
return false;
|
|
38245
38601
|
return labels.some((l) => FEATURE_LABELS.includes(l));
|
|
38246
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
|
+
}
|
|
38247
38634
|
async function discoverFeatures(opts) {
|
|
38248
38635
|
const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold);
|
|
38249
38636
|
if (anchorRepos.length === 0) {
|
|
@@ -38276,42 +38663,15 @@ async function discoverFeatures(opts) {
|
|
|
38276
38663
|
response = listResp;
|
|
38277
38664
|
roadmapRefs = refs;
|
|
38278
38665
|
} catch (err) {
|
|
38279
|
-
|
|
38280
|
-
|
|
38281
|
-
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)}`);
|
|
38282
38668
|
continue;
|
|
38283
38669
|
}
|
|
38284
38670
|
const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
|
|
38285
38671
|
for (const item of items) {
|
|
38286
|
-
const
|
|
38287
|
-
|
|
38288
|
-
|
|
38289
|
-
const comments = item.comments ?? 0;
|
|
38290
|
-
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38291
|
-
const onRoadmap = typeof item.number === "number" && roadmapRefs.has(item.number);
|
|
38292
|
-
let candidate;
|
|
38293
|
-
try {
|
|
38294
|
-
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38295
|
-
featureSignals: {
|
|
38296
|
-
reactions,
|
|
38297
|
-
comments,
|
|
38298
|
-
hasMilestone,
|
|
38299
|
-
wontfixNoContributor,
|
|
38300
|
-
onRoadmap
|
|
38301
|
-
}
|
|
38302
|
-
});
|
|
38303
|
-
} catch (err) {
|
|
38304
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38305
|
-
throw err;
|
|
38306
|
-
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38307
|
-
continue;
|
|
38308
|
-
}
|
|
38309
|
-
const horizon = classifyHorizon({
|
|
38310
|
-
hasMilestone,
|
|
38311
|
-
labels,
|
|
38312
|
-
isOnRoadmap: onRoadmap
|
|
38313
|
-
});
|
|
38314
|
-
candidates.push({ ...candidate, horizon });
|
|
38672
|
+
const candidate = await vetAndClassify(item, opts.vetter, roadmapRefs);
|
|
38673
|
+
if (candidate)
|
|
38674
|
+
candidates.push(candidate);
|
|
38315
38675
|
}
|
|
38316
38676
|
}
|
|
38317
38677
|
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
@@ -38332,10 +38692,8 @@ function buildBroadFeatureSearchQuery(opts) {
|
|
|
38332
38692
|
for (const excl of FEATURE_EXCLUSION_LABELS) {
|
|
38333
38693
|
parts.push(`-label:"${excl}"`);
|
|
38334
38694
|
}
|
|
38335
|
-
|
|
38336
|
-
|
|
38337
|
-
const langClause = languages.map((l) => `language:${l}`).join(" OR ");
|
|
38338
|
-
parts.push(`(${langClause})`);
|
|
38695
|
+
if (opts.language) {
|
|
38696
|
+
parts.push(`language:${opts.language}`);
|
|
38339
38697
|
}
|
|
38340
38698
|
for (const repo of opts.excludeRepos ?? []) {
|
|
38341
38699
|
parts.push(`-repo:${repo}`);
|
|
@@ -38345,8 +38703,19 @@ function buildBroadFeatureSearchQuery(opts) {
|
|
|
38345
38703
|
}
|
|
38346
38704
|
return parts.join(" ");
|
|
38347
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
|
+
}
|
|
38348
38717
|
async function discoverFeaturesBroad(opts) {
|
|
38349
|
-
const
|
|
38718
|
+
const queries = buildBroadFeatureSearchQueries({
|
|
38350
38719
|
languages: opts.languages,
|
|
38351
38720
|
excludeRepos: opts.excludeRepos,
|
|
38352
38721
|
excludeOrgs: opts.excludeOrgs
|
|
@@ -38354,17 +38723,26 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38354
38723
|
const maxToVet = opts.maxToVet ?? DEFAULT_BROAD_MAX_TO_VET;
|
|
38355
38724
|
let items;
|
|
38356
38725
|
try {
|
|
38357
|
-
const
|
|
38358
|
-
|
|
38359
|
-
|
|
38360
|
-
|
|
38361
|
-
|
|
38362
|
-
|
|
38363
|
-
|
|
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);
|
|
38364
38743
|
} catch (err) {
|
|
38365
|
-
|
|
38366
|
-
|
|
38367
|
-
warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
|
|
38744
|
+
rethrowIfFatal(err);
|
|
38745
|
+
warn(MODULE13, `broad feature search failed: ${errorMessage(err)}`);
|
|
38368
38746
|
return {
|
|
38369
38747
|
quickWins: [],
|
|
38370
38748
|
biggerBets: [],
|
|
@@ -38374,32 +38752,9 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38374
38752
|
}
|
|
38375
38753
|
const candidates = [];
|
|
38376
38754
|
for (const item of items) {
|
|
38377
|
-
const
|
|
38378
|
-
|
|
38379
|
-
|
|
38380
|
-
const comments = item.comments ?? 0;
|
|
38381
|
-
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38382
|
-
let candidate;
|
|
38383
|
-
try {
|
|
38384
|
-
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38385
|
-
featureSignals: {
|
|
38386
|
-
reactions,
|
|
38387
|
-
comments,
|
|
38388
|
-
hasMilestone,
|
|
38389
|
-
wontfixNoContributor
|
|
38390
|
-
// Roadmap scraping is per-repo and would require an extra fetch
|
|
38391
|
-
// per unique repo in the broad result set — deliberately skipped
|
|
38392
|
-
// here to keep the broad path cheap. Anchor mode keeps the bonus.
|
|
38393
|
-
}
|
|
38394
|
-
});
|
|
38395
|
-
} catch (err) {
|
|
38396
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38397
|
-
throw err;
|
|
38398
|
-
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38399
|
-
continue;
|
|
38400
|
-
}
|
|
38401
|
-
const horizon = classifyHorizon({ hasMilestone, labels });
|
|
38402
|
-
candidates.push({ ...candidate, horizon });
|
|
38755
|
+
const candidate = await vetAndClassify(item, opts.vetter);
|
|
38756
|
+
if (candidate)
|
|
38757
|
+
candidates.push(candidate);
|
|
38403
38758
|
}
|
|
38404
38759
|
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
38405
38760
|
const split = splitByHorizon(passing, opts.count, opts.splitRatio);
|
|
@@ -38414,19 +38769,16 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38414
38769
|
// ../core/dist/core/gist-state-store.js
|
|
38415
38770
|
var fs3 = __toESM(require("fs"), 1);
|
|
38416
38771
|
var path3 = __toESM(require("path"), 1);
|
|
38417
|
-
var
|
|
38772
|
+
var MODULE14 = "gist-state";
|
|
38418
38773
|
var GIST_DESCRIPTION = "oss-scout-state";
|
|
38419
38774
|
var GIST_FILENAME = "state.json";
|
|
38420
38775
|
var GIST_ID_FILE = "gist-id";
|
|
38421
38776
|
var CACHE_FILE = "state-cache.json";
|
|
38422
|
-
var SEARCH_MAX_PAGES =
|
|
38777
|
+
var SEARCH_MAX_PAGES = 10;
|
|
38423
38778
|
function classifyDegradedReason(err) {
|
|
38424
38779
|
if (isRateLimitError(err))
|
|
38425
38780
|
return "rate_limit";
|
|
38426
38781
|
const status = getHttpStatusCode(err);
|
|
38427
|
-
if (status === 403 && errorMessage(err).toLowerCase().includes("abuse detection")) {
|
|
38428
|
-
return "rate_limit";
|
|
38429
|
-
}
|
|
38430
38782
|
if (status !== void 0 && status >= 500 && status < 600)
|
|
38431
38783
|
return "server";
|
|
38432
38784
|
if (err && typeof err === "object" && "code" in err) {
|
|
@@ -38462,7 +38814,7 @@ var GistStateStore = class {
|
|
|
38462
38814
|
} catch (err) {
|
|
38463
38815
|
if (getHttpStatusCode(err) === 401)
|
|
38464
38816
|
throw err;
|
|
38465
|
-
warn(
|
|
38817
|
+
warn(MODULE14, `API bootstrap failed: ${errorMessage(err)}`);
|
|
38466
38818
|
return this.bootstrapFromCache(classifyDegradedReason(err));
|
|
38467
38819
|
}
|
|
38468
38820
|
}
|
|
@@ -38470,14 +38822,25 @@ var GistStateStore = class {
|
|
|
38470
38822
|
* Push state to the gist. Also writes to local cache as fallback.
|
|
38471
38823
|
*/
|
|
38472
38824
|
async push(state) {
|
|
38473
|
-
this.writeCache(state);
|
|
38474
38825
|
if (!this.gistId) {
|
|
38475
|
-
warn(
|
|
38826
|
+
warn(MODULE14, "No gist ID \u2014 cannot push");
|
|
38827
|
+
this.writeCache(state);
|
|
38476
38828
|
return false;
|
|
38477
38829
|
}
|
|
38478
|
-
|
|
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);
|
|
38479
38842
|
if (json2.length > 9e5) {
|
|
38480
|
-
warn(
|
|
38843
|
+
warn(MODULE14, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
38481
38844
|
return false;
|
|
38482
38845
|
}
|
|
38483
38846
|
try {
|
|
@@ -38487,34 +38850,14 @@ var GistStateStore = class {
|
|
|
38487
38850
|
[GIST_FILENAME]: { content: json2 }
|
|
38488
38851
|
}
|
|
38489
38852
|
});
|
|
38490
|
-
debug(
|
|
38853
|
+
debug(MODULE14, "State pushed to gist");
|
|
38491
38854
|
return true;
|
|
38492
38855
|
} catch (err) {
|
|
38493
|
-
|
|
38494
|
-
|
|
38495
|
-
warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
|
|
38856
|
+
rethrowIfFatal(err);
|
|
38857
|
+
warn(MODULE14, `Failed to push: ${errorMessage(err)}`);
|
|
38496
38858
|
return false;
|
|
38497
38859
|
}
|
|
38498
38860
|
}
|
|
38499
|
-
/**
|
|
38500
|
-
* Pull state from the gist and merge with local state.
|
|
38501
|
-
*/
|
|
38502
|
-
async pull() {
|
|
38503
|
-
if (!this.gistId)
|
|
38504
|
-
return null;
|
|
38505
|
-
try {
|
|
38506
|
-
const state = await this.fetchGistState(this.gistId);
|
|
38507
|
-
if (state) {
|
|
38508
|
-
this.writeCache(state);
|
|
38509
|
-
}
|
|
38510
|
-
return state;
|
|
38511
|
-
} catch (err) {
|
|
38512
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38513
|
-
throw err;
|
|
38514
|
-
warn(MODULE13, `Failed to pull: ${errorMessage(err)}`);
|
|
38515
|
-
return null;
|
|
38516
|
-
}
|
|
38517
|
-
}
|
|
38518
38861
|
/** Get the current gist ID (if known). */
|
|
38519
38862
|
getGistId() {
|
|
38520
38863
|
return this.gistId;
|
|
@@ -38523,35 +38866,40 @@ var GistStateStore = class {
|
|
|
38523
38866
|
async bootstrapFromApi() {
|
|
38524
38867
|
const cachedId = this.readCachedGistId();
|
|
38525
38868
|
if (cachedId) {
|
|
38526
|
-
debug(
|
|
38869
|
+
debug(MODULE14, `Trying cached gist ID: ${cachedId}`);
|
|
38527
38870
|
try {
|
|
38528
|
-
const
|
|
38529
|
-
if (
|
|
38871
|
+
const fetched = await this.fetchGistState(cachedId);
|
|
38872
|
+
if (fetched) {
|
|
38530
38873
|
this.gistId = cachedId;
|
|
38874
|
+
const state = this.mergeCacheInto(fetched);
|
|
38531
38875
|
this.writeCache(state);
|
|
38532
38876
|
return { gistId: cachedId, state, created: false };
|
|
38533
38877
|
}
|
|
38534
38878
|
} catch (err) {
|
|
38535
38879
|
if (getHttpStatusCode(err) !== 404)
|
|
38536
38880
|
throw err;
|
|
38537
|
-
debug(
|
|
38538
|
-
}
|
|
38539
|
-
debug(
|
|
38540
|
-
}
|
|
38541
|
-
const
|
|
38542
|
-
if (
|
|
38543
|
-
debug(
|
|
38544
|
-
this.saveGistId(
|
|
38545
|
-
this.gistId =
|
|
38546
|
-
const
|
|
38547
|
-
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);
|
|
38548
38893
|
this.writeCache(state);
|
|
38549
|
-
return { gistId:
|
|
38894
|
+
return { gistId: search.id, state, created: false };
|
|
38550
38895
|
}
|
|
38551
|
-
warn(
|
|
38896
|
+
warn(MODULE14, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
|
|
38552
38897
|
return this.bootstrapFromCache("unknown");
|
|
38553
38898
|
}
|
|
38554
|
-
|
|
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");
|
|
38555
38903
|
const freshState = ScoutStateSchema.parse({ version: 1 });
|
|
38556
38904
|
const newId = await this.createGist(freshState);
|
|
38557
38905
|
this.saveGistId(newId);
|
|
@@ -38562,7 +38910,7 @@ var GistStateStore = class {
|
|
|
38562
38910
|
bootstrapFromCache(reason) {
|
|
38563
38911
|
const cached2 = this.readCache();
|
|
38564
38912
|
if (cached2) {
|
|
38565
|
-
debug(
|
|
38913
|
+
debug(MODULE14, "Bootstrapped from local cache (degraded mode)");
|
|
38566
38914
|
const cachedId = this.readCachedGistId();
|
|
38567
38915
|
if (cachedId)
|
|
38568
38916
|
this.gistId = cachedId;
|
|
@@ -38574,7 +38922,7 @@ var GistStateStore = class {
|
|
|
38574
38922
|
degradedReason: reason
|
|
38575
38923
|
};
|
|
38576
38924
|
}
|
|
38577
|
-
debug(
|
|
38925
|
+
debug(MODULE14, "No cache available, using fresh state (degraded mode)");
|
|
38578
38926
|
const fresh = ScoutStateSchema.parse({ version: 1 });
|
|
38579
38927
|
return {
|
|
38580
38928
|
gistId: "",
|
|
@@ -38592,12 +38940,17 @@ var GistStateStore = class {
|
|
|
38592
38940
|
return null;
|
|
38593
38941
|
try {
|
|
38594
38942
|
const parsed = JSON.parse(file2.content);
|
|
38595
|
-
return
|
|
38943
|
+
return parseScoutState(parsed);
|
|
38596
38944
|
} catch (err) {
|
|
38597
|
-
warn(
|
|
38945
|
+
warn(MODULE14, `Gist content failed validation: ${errorMessage(err)}`);
|
|
38598
38946
|
return null;
|
|
38599
38947
|
}
|
|
38600
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
|
+
*/
|
|
38601
38954
|
async searchForGist() {
|
|
38602
38955
|
for (let page = 1; page <= SEARCH_MAX_PAGES; page++) {
|
|
38603
38956
|
const { data: gists } = await this.octokit.gists.list({
|
|
@@ -38605,12 +38958,14 @@ var GistStateStore = class {
|
|
|
38605
38958
|
page
|
|
38606
38959
|
});
|
|
38607
38960
|
if (gists.length === 0)
|
|
38608
|
-
|
|
38961
|
+
return { id: null, exhaustive: true };
|
|
38609
38962
|
const match = gists.find((g) => g.description === GIST_DESCRIPTION);
|
|
38610
38963
|
if (match)
|
|
38611
|
-
return match.id;
|
|
38964
|
+
return { id: match.id, exhaustive: true };
|
|
38965
|
+
if (gists.length < 100)
|
|
38966
|
+
return { id: null, exhaustive: true };
|
|
38612
38967
|
}
|
|
38613
|
-
return null;
|
|
38968
|
+
return { id: null, exhaustive: false };
|
|
38614
38969
|
}
|
|
38615
38970
|
async createGist(state) {
|
|
38616
38971
|
const { data } = await this.octokit.gists.create({
|
|
@@ -38630,7 +38985,7 @@ var GistStateStore = class {
|
|
|
38630
38985
|
} catch (err) {
|
|
38631
38986
|
const code = err?.code;
|
|
38632
38987
|
if (code !== "ENOENT") {
|
|
38633
|
-
warn(
|
|
38988
|
+
warn(MODULE14, `Failed to read cached gist ID: ${errorMessage(err)}`);
|
|
38634
38989
|
}
|
|
38635
38990
|
return null;
|
|
38636
38991
|
}
|
|
@@ -38638,14 +38993,25 @@ var GistStateStore = class {
|
|
|
38638
38993
|
saveGistId(id) {
|
|
38639
38994
|
fs3.writeFileSync(getGistIdPath(), id + "\n", { mode: 384 });
|
|
38640
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
|
+
}
|
|
38641
39007
|
readCache() {
|
|
38642
39008
|
try {
|
|
38643
39009
|
const raw = fs3.readFileSync(getCachePath(), "utf-8");
|
|
38644
|
-
return
|
|
39010
|
+
return parseScoutState(JSON.parse(raw));
|
|
38645
39011
|
} catch (err) {
|
|
38646
39012
|
const code = err?.code;
|
|
38647
39013
|
if (code !== "ENOENT") {
|
|
38648
|
-
warn(
|
|
39014
|
+
warn(MODULE14, `Failed to read state cache: ${errorMessage(err)}`);
|
|
38649
39015
|
}
|
|
38650
39016
|
return null;
|
|
38651
39017
|
}
|
|
@@ -38656,22 +39022,61 @@ var GistStateStore = class {
|
|
|
38656
39022
|
mode: 384
|
|
38657
39023
|
});
|
|
38658
39024
|
} catch (err) {
|
|
38659
|
-
warn(
|
|
39025
|
+
warn(MODULE14, `Failed to write cache: ${errorMessage(err)}`);
|
|
38660
39026
|
}
|
|
38661
39027
|
}
|
|
38662
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
|
+
}
|
|
38663
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);
|
|
38664
39065
|
return {
|
|
39066
|
+
...local,
|
|
39067
|
+
...remote,
|
|
38665
39068
|
version: 1,
|
|
38666
|
-
preferences
|
|
39069
|
+
preferences,
|
|
39070
|
+
preferencesUpdatedAt,
|
|
39071
|
+
tombstones,
|
|
38667
39072
|
repoScores: mergeRepoScores(local.repoScores, remote.repoScores),
|
|
38668
39073
|
starredRepos: mergeStarredRepos(local, remote),
|
|
38669
39074
|
starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
|
|
38670
39075
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
38671
39076
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
38672
39077
|
openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
|
|
38673
|
-
savedResults:
|
|
38674
|
-
skippedIssues
|
|
39078
|
+
savedResults: savedResultsFinal,
|
|
39079
|
+
skippedIssues,
|
|
38675
39080
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
38676
39081
|
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
38677
39082
|
gistId: remote.gistId ?? local.gistId
|
|
@@ -38745,7 +39150,7 @@ function pickFresherTimestamp(a, b) {
|
|
|
38745
39150
|
// ../core/dist/core/local-state.js
|
|
38746
39151
|
var fs4 = __toESM(require("fs"), 1);
|
|
38747
39152
|
var path4 = __toESM(require("path"), 1);
|
|
38748
|
-
var
|
|
39153
|
+
var MODULE15 = "local-state";
|
|
38749
39154
|
function getStatePath() {
|
|
38750
39155
|
return path4.join(getDataDir(), "state.json");
|
|
38751
39156
|
}
|
|
@@ -38753,23 +39158,40 @@ function loadLocalState() {
|
|
|
38753
39158
|
const statePath = getStatePath();
|
|
38754
39159
|
try {
|
|
38755
39160
|
const raw = fs4.readFileSync(statePath, "utf-8");
|
|
38756
|
-
return
|
|
39161
|
+
return parseScoutState(JSON.parse(raw));
|
|
38757
39162
|
} catch (err) {
|
|
38758
39163
|
const code = err?.code;
|
|
38759
39164
|
if (code === "ENOENT") {
|
|
38760
39165
|
return ScoutStateSchema.parse({ version: 1 });
|
|
38761
39166
|
}
|
|
38762
|
-
warn(
|
|
39167
|
+
warn(MODULE15, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
|
|
38763
39168
|
try {
|
|
38764
39169
|
const backupPath = `${statePath}.corrupt.${Date.now()}`;
|
|
38765
39170
|
fs4.copyFileSync(statePath, backupPath);
|
|
38766
|
-
warn(
|
|
39171
|
+
warn(MODULE15, `Corrupt state backed up to ${backupPath}`);
|
|
38767
39172
|
} catch (backupErr) {
|
|
38768
|
-
warn(
|
|
39173
|
+
warn(MODULE15, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
38769
39174
|
}
|
|
38770
39175
|
return ScoutStateSchema.parse({ version: 1 });
|
|
38771
39176
|
}
|
|
38772
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
|
+
}
|
|
38773
39195
|
|
|
38774
39196
|
// ../core/dist/scout.js
|
|
38775
39197
|
function offlineModeMessage(reason) {
|
|
@@ -38824,8 +39246,12 @@ function toGistOctokit(octokit) {
|
|
|
38824
39246
|
};
|
|
38825
39247
|
}
|
|
38826
39248
|
async function createScout(config2) {
|
|
39249
|
+
if (config2.logLevel !== void 0) {
|
|
39250
|
+
setLogLevel(config2.logLevel);
|
|
39251
|
+
}
|
|
38827
39252
|
let state;
|
|
38828
39253
|
let gistStore = null;
|
|
39254
|
+
let persistLocal = false;
|
|
38829
39255
|
if (config2.persistence === "provided") {
|
|
38830
39256
|
state = config2.initialState;
|
|
38831
39257
|
} else if (config2.persistence === "gist") {
|
|
@@ -38842,36 +39268,61 @@ async function createScout(config2) {
|
|
|
38842
39268
|
state.gistId = result.gistId;
|
|
38843
39269
|
}
|
|
38844
39270
|
} else {
|
|
38845
|
-
state =
|
|
39271
|
+
state = loadLocalState();
|
|
39272
|
+
persistLocal = true;
|
|
38846
39273
|
}
|
|
38847
|
-
return new OssScout(config2.githubToken, state, gistStore);
|
|
39274
|
+
return new OssScout(config2.githubToken, state, gistStore, { persistLocal });
|
|
38848
39275
|
}
|
|
38849
39276
|
var OssScout = class {
|
|
38850
39277
|
githubToken;
|
|
38851
39278
|
gistStore;
|
|
38852
39279
|
state;
|
|
38853
39280
|
dirty = false;
|
|
38854
|
-
|
|
39281
|
+
/** When true, checkpoint() also writes ~/.oss-scout/state.json. */
|
|
39282
|
+
persistLocal;
|
|
39283
|
+
constructor(githubToken, initialState, gistStore = null, opts = {}) {
|
|
38855
39284
|
this.githubToken = githubToken;
|
|
38856
39285
|
this.gistStore = gistStore;
|
|
38857
39286
|
this.state = initialState;
|
|
39287
|
+
this.persistLocal = opts.persistLocal ?? false;
|
|
38858
39288
|
}
|
|
38859
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
|
+
}
|
|
38860
39298
|
/**
|
|
38861
39299
|
* Multi-strategy issue search. Returns scored, sorted candidates.
|
|
38862
39300
|
* Automatically culls expired skip entries and filters skipped issues.
|
|
38863
39301
|
*/
|
|
38864
39302
|
async search(options) {
|
|
39303
|
+
this.evictStaleCacheEntries();
|
|
38865
39304
|
this.cullExpiredSkips();
|
|
38866
39305
|
const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
38867
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;
|
|
38868
39313
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
38869
39314
|
maxResults: options?.maxResults,
|
|
38870
39315
|
strategies: options?.strategies,
|
|
38871
39316
|
skippedUrls,
|
|
38872
|
-
preferLanguages
|
|
38873
|
-
preferRepos
|
|
39317
|
+
preferLanguages,
|
|
39318
|
+
preferRepos,
|
|
39319
|
+
diversityRatio,
|
|
39320
|
+
interPhaseDelayMs: options?.interPhaseDelayMs,
|
|
39321
|
+
broadPhaseDelayMs: options?.broadPhaseDelayMs
|
|
38874
39322
|
});
|
|
39323
|
+
for (const c of candidates) {
|
|
39324
|
+
this.updateRepoSignalsFromHealth(c.projectHealth);
|
|
39325
|
+
}
|
|
38875
39326
|
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38876
39327
|
this.dirty = true;
|
|
38877
39328
|
return {
|
|
@@ -38882,6 +39333,28 @@ var OssScout = class {
|
|
|
38882
39333
|
strategiesUsed
|
|
38883
39334
|
};
|
|
38884
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
|
+
}
|
|
38885
39358
|
/**
|
|
38886
39359
|
* Vet a single issue URL for claimability.
|
|
38887
39360
|
*/
|
|
@@ -38904,6 +39377,7 @@ var OssScout = class {
|
|
|
38904
39377
|
* preferences and excluded repos/orgs.
|
|
38905
39378
|
*/
|
|
38906
39379
|
async features(options) {
|
|
39380
|
+
this.evictStaleCacheEntries();
|
|
38907
39381
|
const count = options?.count ?? 10;
|
|
38908
39382
|
const octokit = getOctokit(this.githubToken);
|
|
38909
39383
|
const vetter = new IssueVetter(octokit, this);
|
|
@@ -38935,6 +39409,7 @@ var OssScout = class {
|
|
|
38935
39409
|
* Optionally prunes unavailable issues from saved results.
|
|
38936
39410
|
*/
|
|
38937
39411
|
async vetList(options) {
|
|
39412
|
+
this.evictStaleCacheEntries();
|
|
38938
39413
|
const saved = this.getSavedResults();
|
|
38939
39414
|
const concurrency = options?.concurrency ?? 5;
|
|
38940
39415
|
const results = [];
|
|
@@ -38950,6 +39425,7 @@ var OssScout = class {
|
|
|
38950
39425
|
number: item.number,
|
|
38951
39426
|
title: item.title,
|
|
38952
39427
|
status: this.classifyVetResult(candidate),
|
|
39428
|
+
ok: true,
|
|
38953
39429
|
recommendation: candidate.recommendation,
|
|
38954
39430
|
viabilityScore: candidate.viabilityScore
|
|
38955
39431
|
});
|
|
@@ -38966,6 +39442,7 @@ var OssScout = class {
|
|
|
38966
39442
|
number: item.number,
|
|
38967
39443
|
title: item.title,
|
|
38968
39444
|
status: isGone ? "closed" : "error",
|
|
39445
|
+
ok: false,
|
|
38969
39446
|
errorMessage: errorMessage(error48)
|
|
38970
39447
|
});
|
|
38971
39448
|
}).finally(() => {
|
|
@@ -38991,17 +39468,41 @@ var OssScout = class {
|
|
|
38991
39468
|
hasPR: results.filter((r) => r.status === "has_pr").length,
|
|
38992
39469
|
errors: results.filter((r) => r.status === "error").length
|
|
38993
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;
|
|
38994
39491
|
let prunedCount;
|
|
38995
39492
|
if (options?.prune) {
|
|
38996
39493
|
const unavailableUrls = new Set(results.filter((r) => r.status !== "still_available").map((r) => r.issueUrl));
|
|
38997
39494
|
const before = (this.state.savedResults ?? []).length;
|
|
38998
39495
|
this.state.savedResults = (this.state.savedResults ?? []).filter((r) => !unavailableUrls.has(r.issueUrl));
|
|
38999
39496
|
prunedCount = before - (this.state.savedResults?.length ?? 0);
|
|
39497
|
+
if (prunedCount > 0)
|
|
39498
|
+
this.addTombstones([...unavailableUrls]);
|
|
39000
39499
|
this.dirty = true;
|
|
39001
39500
|
}
|
|
39002
|
-
return { results, summary, prunedCount };
|
|
39501
|
+
return { results, summary, prunedCount, transitions };
|
|
39003
39502
|
}
|
|
39004
39503
|
classifyVetResult(candidate) {
|
|
39504
|
+
if (candidate.issueState === "closed")
|
|
39505
|
+
return "closed";
|
|
39005
39506
|
const checks = candidate.vettingResult.checks;
|
|
39006
39507
|
if (!checks.noExistingPR)
|
|
39007
39508
|
return "has_pr";
|
|
@@ -39036,19 +39537,35 @@ var OssScout = class {
|
|
|
39036
39537
|
getProjectCategories() {
|
|
39037
39538
|
return this.state.preferences.projectCategories;
|
|
39038
39539
|
}
|
|
39540
|
+
/** Configured GitHub username (used to classify own vs competing PRs, #166). */
|
|
39541
|
+
getGitHubUsername() {
|
|
39542
|
+
return this.state.preferences.githubUsername;
|
|
39543
|
+
}
|
|
39039
39544
|
getRepoScore(repo) {
|
|
39040
39545
|
const score = this.state.repoScores[repo];
|
|
39041
39546
|
return score ? score.score : null;
|
|
39042
39547
|
}
|
|
39043
39548
|
/**
|
|
39044
|
-
*
|
|
39045
|
-
*
|
|
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).
|
|
39046
39563
|
*/
|
|
39047
39564
|
getSLMTriageConfig() {
|
|
39048
|
-
|
|
39049
|
-
|
|
39050
|
-
|
|
39051
|
-
};
|
|
39565
|
+
const model = this.state.preferences.slmTriageModel ?? "";
|
|
39566
|
+
if (!model)
|
|
39567
|
+
return null;
|
|
39568
|
+
return { model, host: this.state.preferences.slmTriageHost ?? "" };
|
|
39052
39569
|
}
|
|
39053
39570
|
/** Get current preferences (read-only). */
|
|
39054
39571
|
getPreferences() {
|
|
@@ -39102,6 +39619,73 @@ var OssScout = class {
|
|
|
39102
39619
|
];
|
|
39103
39620
|
this.dirty = true;
|
|
39104
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
|
+
}
|
|
39105
39689
|
/**
|
|
39106
39690
|
* Update repo score with observed signals.
|
|
39107
39691
|
*/
|
|
@@ -39136,6 +39720,7 @@ var OssScout = class {
|
|
|
39136
39720
|
*/
|
|
39137
39721
|
updatePreferences(updates) {
|
|
39138
39722
|
this.state.preferences = { ...this.state.preferences, ...updates };
|
|
39723
|
+
this.state.preferencesUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
39139
39724
|
this.dirty = true;
|
|
39140
39725
|
}
|
|
39141
39726
|
/**
|
|
@@ -39185,9 +39770,25 @@ var OssScout = class {
|
|
|
39185
39770
|
* Clear all saved results.
|
|
39186
39771
|
*/
|
|
39187
39772
|
clearResults() {
|
|
39773
|
+
this.addTombstones((this.state.savedResults ?? []).map((r) => r.issueUrl));
|
|
39188
39774
|
this.state.savedResults = [];
|
|
39189
39775
|
this.dirty = true;
|
|
39190
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
|
+
}
|
|
39191
39792
|
// ── Skip List ───────────────────────────────────────────────────────
|
|
39192
39793
|
/**
|
|
39193
39794
|
* Skip an issue — excludes it from future searches. Auto-culled after 90 days.
|
|
@@ -39221,13 +39822,17 @@ var OssScout = class {
|
|
|
39221
39822
|
* Remove a specific issue from the skip list.
|
|
39222
39823
|
*/
|
|
39223
39824
|
unskipIssue(url2) {
|
|
39825
|
+
const before = (this.state.skippedIssues ?? []).length;
|
|
39224
39826
|
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url2);
|
|
39827
|
+
if (this.state.skippedIssues.length < before)
|
|
39828
|
+
this.addTombstones([url2]);
|
|
39225
39829
|
this.dirty = true;
|
|
39226
39830
|
}
|
|
39227
39831
|
/**
|
|
39228
39832
|
* Clear all skipped issues.
|
|
39229
39833
|
*/
|
|
39230
39834
|
clearSkippedIssues() {
|
|
39835
|
+
this.addTombstones((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
39231
39836
|
this.state.skippedIssues = [];
|
|
39232
39837
|
this.dirty = true;
|
|
39233
39838
|
}
|
|
@@ -39271,6 +39876,13 @@ var OssScout = class {
|
|
|
39271
39876
|
if (!ok)
|
|
39272
39877
|
return false;
|
|
39273
39878
|
}
|
|
39879
|
+
if (this.persistLocal) {
|
|
39880
|
+
try {
|
|
39881
|
+
saveLocalState(this.state);
|
|
39882
|
+
} catch {
|
|
39883
|
+
return false;
|
|
39884
|
+
}
|
|
39885
|
+
}
|
|
39274
39886
|
this.dirty = false;
|
|
39275
39887
|
return true;
|
|
39276
39888
|
}
|
|
@@ -39309,8 +39921,152 @@ var OssScout = class {
|
|
|
39309
39921
|
}
|
|
39310
39922
|
};
|
|
39311
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
|
+
|
|
39312
40068
|
// src/tools.ts
|
|
39313
|
-
var TOOL_TIMEOUT_MS =
|
|
40069
|
+
var TOOL_TIMEOUT_MS = 3e5;
|
|
39314
40070
|
function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
|
|
39315
40071
|
let timeoutId;
|
|
39316
40072
|
const timer = new Promise((_, reject) => {
|
|
@@ -39335,9 +40091,18 @@ function registerTools(server, scout) {
|
|
|
39335
40091
|
),
|
|
39336
40092
|
preferRepos: external_exports3.array(external_exports3.string()).optional().describe(
|
|
39337
40093
|
"Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."
|
|
40094
|
+
),
|
|
40095
|
+
diversityRatio: external_exports3.number().min(0).max(1).optional().describe(
|
|
40096
|
+
"Diversity counterweight (#1244): fraction of result slots reserved for candidates that matched NEITHER preference list. Counters echo-chamber bias when boosts accumulate over time. 0 disables, 1 reserves every slot for diversity. Default 0."
|
|
39338
40097
|
)
|
|
39339
40098
|
},
|
|
39340
|
-
async ({
|
|
40099
|
+
async ({
|
|
40100
|
+
maxResults,
|
|
40101
|
+
strategies,
|
|
40102
|
+
preferLanguages,
|
|
40103
|
+
preferRepos,
|
|
40104
|
+
diversityRatio
|
|
40105
|
+
}) => {
|
|
39341
40106
|
try {
|
|
39342
40107
|
const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
|
|
39343
40108
|
const result = await withTimeout(
|
|
@@ -39345,7 +40110,12 @@ function registerTools(server, scout) {
|
|
|
39345
40110
|
maxResults: maxResults ?? 10,
|
|
39346
40111
|
strategies: parsedStrategies,
|
|
39347
40112
|
preferLanguages,
|
|
39348
|
-
preferRepos
|
|
40113
|
+
preferRepos,
|
|
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
|
|
39349
40119
|
})
|
|
39350
40120
|
);
|
|
39351
40121
|
scout.saveResults(result.candidates);
|
|
@@ -39426,6 +40196,24 @@ function registerTools(server, scout) {
|
|
|
39426
40196
|
};
|
|
39427
40197
|
}
|
|
39428
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
|
+
);
|
|
39429
40217
|
server.tool(
|
|
39430
40218
|
"skip",
|
|
39431
40219
|
"Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.",
|
|
@@ -39456,7 +40244,7 @@ function registerTools(server, scout) {
|
|
|
39456
40244
|
const count = scout.getSkippedIssues().length;
|
|
39457
40245
|
scout.clearSkippedIssues();
|
|
39458
40246
|
const synced2 = await scout.checkpoint();
|
|
39459
|
-
const syncNote2 = synced2 ? "" : " (
|
|
40247
|
+
const syncNote2 = synced2 ? "" : " (warning: failed to persist to disk)";
|
|
39460
40248
|
return {
|
|
39461
40249
|
content: [
|
|
39462
40250
|
{
|
|
@@ -39470,7 +40258,7 @@ function registerTools(server, scout) {
|
|
|
39470
40258
|
const wasPresent = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
39471
40259
|
scout.unskipIssue(issueUrl);
|
|
39472
40260
|
const synced2 = await scout.checkpoint();
|
|
39473
|
-
const syncNote2 = synced2 ? "" : " (
|
|
40261
|
+
const syncNote2 = synced2 ? "" : " (warning: failed to persist to disk)";
|
|
39474
40262
|
return {
|
|
39475
40263
|
content: [
|
|
39476
40264
|
{
|
|
@@ -39480,6 +40268,8 @@ function registerTools(server, scout) {
|
|
|
39480
40268
|
]
|
|
39481
40269
|
};
|
|
39482
40270
|
}
|
|
40271
|
+
validateUrl(issueUrl);
|
|
40272
|
+
validateGitHubUrl(issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
39483
40273
|
const alreadySkipped = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
39484
40274
|
if (alreadySkipped) {
|
|
39485
40275
|
return {
|
|
@@ -39496,7 +40286,7 @@ function registerTools(server, scout) {
|
|
|
39496
40286
|
} : void 0;
|
|
39497
40287
|
scout.skipIssue(issueUrl, metadata);
|
|
39498
40288
|
const synced = await scout.checkpoint();
|
|
39499
|
-
const syncNote = synced ? "" : " (
|
|
40289
|
+
const syncNote = synced ? "" : " (warning: failed to persist to disk)";
|
|
39500
40290
|
return {
|
|
39501
40291
|
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }]
|
|
39502
40292
|
};
|
|
@@ -39509,101 +40299,31 @@ function registerTools(server, scout) {
|
|
|
39509
40299
|
}
|
|
39510
40300
|
}
|
|
39511
40301
|
);
|
|
39512
|
-
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
39513
|
-
const ARRAY_KEYS = /* @__PURE__ */ new Set([
|
|
39514
|
-
"languages",
|
|
39515
|
-
"labels",
|
|
39516
|
-
"projectCategories",
|
|
39517
|
-
"excludeRepos",
|
|
39518
|
-
"excludeOrgs",
|
|
39519
|
-
"aiPolicyBlocklist",
|
|
39520
|
-
"scope",
|
|
39521
|
-
"defaultStrategy"
|
|
39522
|
-
]);
|
|
39523
|
-
const NUMBER_KEYS = /* @__PURE__ */ new Set([
|
|
39524
|
-
"minStars",
|
|
39525
|
-
"maxIssueAgeDays",
|
|
39526
|
-
"minRepoScoreThreshold",
|
|
39527
|
-
"interPhaseDelayMs",
|
|
39528
|
-
"broadPhaseDelayMs",
|
|
39529
|
-
"skipBroadWhenSufficientResults",
|
|
39530
|
-
"featuresAnchorThreshold",
|
|
39531
|
-
"featuresSplitRatio"
|
|
39532
|
-
]);
|
|
39533
|
-
const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
|
|
39534
40302
|
server.tool(
|
|
39535
40303
|
"config-set",
|
|
39536
40304
|
"Update an oss-scout configuration preference",
|
|
39537
40305
|
{
|
|
39538
40306
|
key: external_exports3.string().describe(
|
|
39539
|
-
|
|
40307
|
+
`Preference key to update. One of: ${SORTED_PREFERENCE_KEYS.join(", ")}`
|
|
39540
40308
|
),
|
|
39541
40309
|
value: external_exports3.string().describe(
|
|
39542
|
-
|
|
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.'
|
|
39543
40311
|
)
|
|
39544
40312
|
},
|
|
39545
40313
|
async ({ key, value }) => {
|
|
39546
|
-
|
|
39547
|
-
|
|
39548
|
-
|
|
39549
|
-
|
|
39550
|
-
|
|
39551
|
-
text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`
|
|
39552
|
-
}
|
|
39553
|
-
],
|
|
39554
|
-
isError: true
|
|
39555
|
-
};
|
|
39556
|
-
}
|
|
39557
|
-
let parsed;
|
|
39558
|
-
if (ARRAY_KEYS.has(key)) {
|
|
39559
|
-
try {
|
|
39560
|
-
parsed = JSON.parse(value);
|
|
39561
|
-
if (!Array.isArray(parsed)) {
|
|
39562
|
-
parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39563
|
-
}
|
|
39564
|
-
} catch {
|
|
39565
|
-
parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39566
|
-
}
|
|
39567
|
-
} else if (NUMBER_KEYS.has(key)) {
|
|
39568
|
-
const num = Number(value);
|
|
39569
|
-
if (isNaN(num)) {
|
|
39570
|
-
return {
|
|
39571
|
-
content: [
|
|
39572
|
-
{ type: "text", text: `Invalid number for "${key}": "${value}"` }
|
|
39573
|
-
],
|
|
39574
|
-
isError: true
|
|
39575
|
-
};
|
|
39576
|
-
}
|
|
39577
|
-
parsed = num;
|
|
39578
|
-
} else if (BOOLEAN_KEYS.has(key)) {
|
|
39579
|
-
const lower = value.toLowerCase();
|
|
39580
|
-
if (lower === "true" || lower === "yes") parsed = true;
|
|
39581
|
-
else if (lower === "false" || lower === "no") parsed = false;
|
|
39582
|
-
else {
|
|
39583
|
-
return {
|
|
39584
|
-
content: [
|
|
39585
|
-
{
|
|
39586
|
-
type: "text",
|
|
39587
|
-
text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`
|
|
39588
|
-
}
|
|
39589
|
-
],
|
|
39590
|
-
isError: true
|
|
39591
|
-
};
|
|
39592
|
-
}
|
|
39593
|
-
} else {
|
|
39594
|
-
parsed = value;
|
|
39595
|
-
}
|
|
39596
|
-
const currentPrefs = scout.getPreferences();
|
|
39597
|
-
const candidate = { ...currentPrefs, [key]: parsed };
|
|
39598
|
-
const result = ScoutPreferencesSchema.safeParse(candidate);
|
|
39599
|
-
if (!result.success) {
|
|
39600
|
-
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);
|
|
39601
40319
|
return {
|
|
39602
|
-
content: [{ type: "text", text:
|
|
40320
|
+
content: [{ type: "text", text: msg }],
|
|
39603
40321
|
isError: true
|
|
39604
40322
|
};
|
|
39605
40323
|
}
|
|
39606
|
-
scout.updatePreferences({
|
|
40324
|
+
scout.updatePreferences({
|
|
40325
|
+
[key]: validated[key]
|
|
40326
|
+
});
|
|
39607
40327
|
await scout.checkpoint();
|
|
39608
40328
|
const updated = scout.getPreferences();
|
|
39609
40329
|
return {
|
|
@@ -39663,25 +40383,34 @@ function registerResources(server, scout) {
|
|
|
39663
40383
|
}
|
|
39664
40384
|
|
|
39665
40385
|
// src/index.ts
|
|
39666
|
-
|
|
39667
|
-
|
|
39668
|
-
|
|
39669
|
-
|
|
39670
|
-
|
|
39671
|
-
|
|
39672
|
-
process.exit(1);
|
|
39673
|
-
}
|
|
39674
|
-
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) {
|
|
39675
40392
|
const server = new McpServer(
|
|
39676
|
-
{ name:
|
|
40393
|
+
{ name: SERVER_INFO.name, version: SERVER_INFO.version },
|
|
39677
40394
|
{ capabilities: { resources: {}, tools: {} } }
|
|
39678
40395
|
);
|
|
39679
40396
|
registerTools(server, scout);
|
|
39680
40397
|
registerResources(server, scout);
|
|
39681
|
-
|
|
39682
|
-
await server.connect(transport);
|
|
40398
|
+
return server;
|
|
39683
40399
|
}
|
|
39684
|
-
|
|
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) => {
|
|
39685
40414
|
process.stderr.write(
|
|
39686
40415
|
`oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
|
|
39687
40416
|
`
|