@oss-scout/mcp 0.5.0 → 0.7.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/mcp-server.bundle.cjs +1203 -151
- package/dist/tools.js +52 -1
- package/package.json +2 -2
|
@@ -3099,12 +3099,15 @@ var require_data = __commonJS({
|
|
|
3099
3099
|
}
|
|
3100
3100
|
});
|
|
3101
3101
|
|
|
3102
|
-
// ../../node_modules/.pnpm/fast-uri@3.1.
|
|
3102
|
+
// ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/utils.js
|
|
3103
3103
|
var require_utils = __commonJS({
|
|
3104
|
-
"../../node_modules/.pnpm/fast-uri@3.1.
|
|
3104
|
+
"../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/utils.js"(exports2, module2) {
|
|
3105
3105
|
"use strict";
|
|
3106
3106
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3107
3107
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3108
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3109
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3110
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3108
3111
|
function stringArrayToHexStripped(input) {
|
|
3109
3112
|
let acc = "";
|
|
3110
3113
|
let code = 0;
|
|
@@ -3297,27 +3300,77 @@ var require_utils = __commonJS({
|
|
|
3297
3300
|
}
|
|
3298
3301
|
return output.join("");
|
|
3299
3302
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3303
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3304
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3305
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3306
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3307
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3308
|
+
re.lastIndex = 0;
|
|
3309
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3310
|
+
}
|
|
3311
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3312
|
+
if (input.indexOf("%") === -1) {
|
|
3313
|
+
return input;
|
|
3310
3314
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3315
|
+
let output = "";
|
|
3316
|
+
for (let i = 0; i < input.length; i++) {
|
|
3317
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3318
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3319
|
+
if (isHexPair(hex3)) {
|
|
3320
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3321
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3322
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3323
|
+
output += decoded;
|
|
3324
|
+
} else {
|
|
3325
|
+
output += "%" + normalizedHex;
|
|
3326
|
+
}
|
|
3327
|
+
i += 2;
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
output += input[i];
|
|
3313
3332
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3333
|
+
return output;
|
|
3334
|
+
}
|
|
3335
|
+
function normalizePathEncoding(input) {
|
|
3336
|
+
let output = "";
|
|
3337
|
+
for (let i = 0; i < input.length; i++) {
|
|
3338
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3339
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3340
|
+
if (isHexPair(hex3)) {
|
|
3341
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3342
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3343
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3344
|
+
output += decoded;
|
|
3345
|
+
} else {
|
|
3346
|
+
output += "%" + normalizedHex;
|
|
3347
|
+
}
|
|
3348
|
+
i += 2;
|
|
3349
|
+
continue;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
if (isPathCharacter(input[i])) {
|
|
3353
|
+
output += input[i];
|
|
3354
|
+
} else {
|
|
3355
|
+
output += escape(input[i]);
|
|
3356
|
+
}
|
|
3316
3357
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3358
|
+
return output;
|
|
3359
|
+
}
|
|
3360
|
+
function escapePreservingEscapes(input) {
|
|
3361
|
+
let output = "";
|
|
3362
|
+
for (let i = 0; i < input.length; i++) {
|
|
3363
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3364
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3365
|
+
if (isHexPair(hex3)) {
|
|
3366
|
+
output += "%" + hex3.toUpperCase();
|
|
3367
|
+
i += 2;
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
output += escape(input[i]);
|
|
3319
3372
|
}
|
|
3320
|
-
return
|
|
3373
|
+
return output;
|
|
3321
3374
|
}
|
|
3322
3375
|
function recomposeAuthority(component) {
|
|
3323
3376
|
const uriTokens = [];
|
|
@@ -3332,7 +3385,7 @@ var require_utils = __commonJS({
|
|
|
3332
3385
|
if (ipV6res.isIPV6 === true) {
|
|
3333
3386
|
host = `[${ipV6res.escapedHost}]`;
|
|
3334
3387
|
} else {
|
|
3335
|
-
host =
|
|
3388
|
+
host = reescapeHostDelimiters(host, false);
|
|
3336
3389
|
}
|
|
3337
3390
|
}
|
|
3338
3391
|
uriTokens.push(host);
|
|
@@ -3346,7 +3399,10 @@ var require_utils = __commonJS({
|
|
|
3346
3399
|
module2.exports = {
|
|
3347
3400
|
nonSimpleDomain,
|
|
3348
3401
|
recomposeAuthority,
|
|
3349
|
-
|
|
3402
|
+
reescapeHostDelimiters,
|
|
3403
|
+
normalizePercentEncoding,
|
|
3404
|
+
normalizePathEncoding,
|
|
3405
|
+
escapePreservingEscapes,
|
|
3350
3406
|
removeDotSegments,
|
|
3351
3407
|
isIPv4,
|
|
3352
3408
|
isUUID,
|
|
@@ -3356,9 +3412,9 @@ var require_utils = __commonJS({
|
|
|
3356
3412
|
}
|
|
3357
3413
|
});
|
|
3358
3414
|
|
|
3359
|
-
// ../../node_modules/.pnpm/fast-uri@3.1.
|
|
3415
|
+
// ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/schemes.js
|
|
3360
3416
|
var require_schemes = __commonJS({
|
|
3361
|
-
"../../node_modules/.pnpm/fast-uri@3.1.
|
|
3417
|
+
"../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/schemes.js"(exports2, module2) {
|
|
3362
3418
|
"use strict";
|
|
3363
3419
|
var { isUUID } = require_utils();
|
|
3364
3420
|
var URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
|
|
@@ -3566,16 +3622,16 @@ var require_schemes = __commonJS({
|
|
|
3566
3622
|
}
|
|
3567
3623
|
});
|
|
3568
3624
|
|
|
3569
|
-
// ../../node_modules/.pnpm/fast-uri@3.1.
|
|
3625
|
+
// ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/index.js
|
|
3570
3626
|
var require_fast_uri = __commonJS({
|
|
3571
|
-
"../../node_modules/.pnpm/fast-uri@3.1.
|
|
3627
|
+
"../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/index.js"(exports2, module2) {
|
|
3572
3628
|
"use strict";
|
|
3573
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3629
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3574
3630
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3575
3631
|
function normalize(uri, options) {
|
|
3576
3632
|
if (typeof uri === "string") {
|
|
3577
3633
|
uri = /** @type {T} */
|
|
3578
|
-
|
|
3634
|
+
normalizeString(uri, options);
|
|
3579
3635
|
} else if (typeof uri === "object") {
|
|
3580
3636
|
uri = /** @type {T} */
|
|
3581
3637
|
parse4(serialize(uri, options), options);
|
|
@@ -3642,19 +3698,9 @@ var require_fast_uri = __commonJS({
|
|
|
3642
3698
|
return target;
|
|
3643
3699
|
}
|
|
3644
3700
|
function equal(uriA, uriB, options) {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
} else if (typeof uriA === "object") {
|
|
3649
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3650
|
-
}
|
|
3651
|
-
if (typeof uriB === "string") {
|
|
3652
|
-
uriB = unescape(uriB);
|
|
3653
|
-
uriB = serialize(normalizeComponentEncoding(parse4(uriB, options), true), { ...options, skipEscape: true });
|
|
3654
|
-
} else if (typeof uriB === "object") {
|
|
3655
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3656
|
-
}
|
|
3657
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3701
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3702
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3703
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3658
3704
|
}
|
|
3659
3705
|
function serialize(cmpts, opts) {
|
|
3660
3706
|
const component = {
|
|
@@ -3679,12 +3725,12 @@ var require_fast_uri = __commonJS({
|
|
|
3679
3725
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3680
3726
|
if (component.path !== void 0) {
|
|
3681
3727
|
if (!options.skipEscape) {
|
|
3682
|
-
component.path =
|
|
3728
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3683
3729
|
if (component.scheme !== void 0) {
|
|
3684
3730
|
component.path = component.path.split("%3A").join(":");
|
|
3685
3731
|
}
|
|
3686
3732
|
} else {
|
|
3687
|
-
component.path =
|
|
3733
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3688
3734
|
}
|
|
3689
3735
|
}
|
|
3690
3736
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3719,7 +3765,16 @@ var require_fast_uri = __commonJS({
|
|
|
3719
3765
|
return uriTokens.join("");
|
|
3720
3766
|
}
|
|
3721
3767
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3722
|
-
function
|
|
3768
|
+
function getParseError(parsed, matches) {
|
|
3769
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3770
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3771
|
+
}
|
|
3772
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3773
|
+
return "URI port is malformed.";
|
|
3774
|
+
}
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
function parseWithStatus(uri, opts) {
|
|
3723
3778
|
const options = Object.assign({}, opts);
|
|
3724
3779
|
const parsed = {
|
|
3725
3780
|
scheme: void 0,
|
|
@@ -3730,6 +3785,7 @@ var require_fast_uri = __commonJS({
|
|
|
3730
3785
|
query: void 0,
|
|
3731
3786
|
fragment: void 0
|
|
3732
3787
|
};
|
|
3788
|
+
let malformedAuthorityOrPort = false;
|
|
3733
3789
|
let isIP = false;
|
|
3734
3790
|
if (options.reference === "suffix") {
|
|
3735
3791
|
if (options.scheme) {
|
|
@@ -3750,6 +3806,11 @@ var require_fast_uri = __commonJS({
|
|
|
3750
3806
|
if (isNaN(parsed.port)) {
|
|
3751
3807
|
parsed.port = matches[5];
|
|
3752
3808
|
}
|
|
3809
|
+
const parseError = getParseError(parsed, matches);
|
|
3810
|
+
if (parseError !== void 0) {
|
|
3811
|
+
parsed.error = parsed.error || parseError;
|
|
3812
|
+
malformedAuthorityOrPort = true;
|
|
3813
|
+
}
|
|
3753
3814
|
if (parsed.host) {
|
|
3754
3815
|
const ipv4result = isIPv4(parsed.host);
|
|
3755
3816
|
if (ipv4result === false) {
|
|
@@ -3788,14 +3849,18 @@ var require_fast_uri = __commonJS({
|
|
|
3788
3849
|
parsed.scheme = unescape(parsed.scheme);
|
|
3789
3850
|
}
|
|
3790
3851
|
if (parsed.host !== void 0) {
|
|
3791
|
-
parsed.host = unescape(parsed.host);
|
|
3852
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3792
3853
|
}
|
|
3793
3854
|
}
|
|
3794
3855
|
if (parsed.path) {
|
|
3795
|
-
parsed.path =
|
|
3856
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3796
3857
|
}
|
|
3797
3858
|
if (parsed.fragment) {
|
|
3798
|
-
|
|
3859
|
+
try {
|
|
3860
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3861
|
+
} catch {
|
|
3862
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3863
|
+
}
|
|
3799
3864
|
}
|
|
3800
3865
|
}
|
|
3801
3866
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3804,7 +3869,29 @@ var require_fast_uri = __commonJS({
|
|
|
3804
3869
|
} else {
|
|
3805
3870
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3806
3871
|
}
|
|
3807
|
-
return parsed;
|
|
3872
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3873
|
+
}
|
|
3874
|
+
function parse4(uri, opts) {
|
|
3875
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3876
|
+
}
|
|
3877
|
+
function normalizeString(uri, opts) {
|
|
3878
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3879
|
+
}
|
|
3880
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3881
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3882
|
+
return {
|
|
3883
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3884
|
+
malformedAuthorityOrPort
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
function normalizeComparableURI(uri, opts) {
|
|
3888
|
+
if (typeof uri === "string") {
|
|
3889
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3890
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3891
|
+
}
|
|
3892
|
+
if (typeof uri === "object") {
|
|
3893
|
+
return serialize(uri, opts);
|
|
3894
|
+
}
|
|
3808
3895
|
}
|
|
3809
3896
|
var fastUri = {
|
|
3810
3897
|
SCHEMES,
|
|
@@ -32107,8 +32194,8 @@ function isPlainObject4(value) {
|
|
|
32107
32194
|
}
|
|
32108
32195
|
var noop = () => "";
|
|
32109
32196
|
async function fetchWrapper(requestOptions) {
|
|
32110
|
-
const
|
|
32111
|
-
if (!
|
|
32197
|
+
const fetch2 = requestOptions.request?.fetch || globalThis.fetch;
|
|
32198
|
+
if (!fetch2) {
|
|
32112
32199
|
throw new Error(
|
|
32113
32200
|
"fetch is not set. Please pass a fetch implementation as new Octokit({ request: { fetch }}). Learn more at https://github.com/octokit/octokit.js/#fetch-missing"
|
|
32114
32201
|
);
|
|
@@ -32124,7 +32211,7 @@ async function fetchWrapper(requestOptions) {
|
|
|
32124
32211
|
);
|
|
32125
32212
|
let fetchResponse;
|
|
32126
32213
|
try {
|
|
32127
|
-
fetchResponse = await
|
|
32214
|
+
fetchResponse = await fetch2(requestOptions.url, {
|
|
32128
32215
|
method: requestOptions.method,
|
|
32129
32216
|
body,
|
|
32130
32217
|
redirect: requestOptions.request?.redirect,
|
|
@@ -35763,6 +35850,19 @@ var ContributionGuidelinesSchema = external_exports3.object({
|
|
|
35763
35850
|
claRequired: external_exports3.boolean().optional(),
|
|
35764
35851
|
rawContent: external_exports3.string().optional()
|
|
35765
35852
|
});
|
|
35853
|
+
var LinkedPRSchema = external_exports3.object({
|
|
35854
|
+
number: external_exports3.number(),
|
|
35855
|
+
author: external_exports3.string(),
|
|
35856
|
+
state: external_exports3.enum(["open", "closed"]),
|
|
35857
|
+
merged: external_exports3.boolean(),
|
|
35858
|
+
url: external_exports3.string(),
|
|
35859
|
+
/**
|
|
35860
|
+
* ISO-8601 timestamp of the linked PR's last update. Optional so existing
|
|
35861
|
+
* persisted state validates unchanged. Used by `isLinkedPRStalled` to
|
|
35862
|
+
* surface stale revive opportunities (#97).
|
|
35863
|
+
*/
|
|
35864
|
+
updatedAt: external_exports3.string().optional()
|
|
35865
|
+
});
|
|
35766
35866
|
var IssueVettingResultSchema = external_exports3.object({
|
|
35767
35867
|
passedAllChecks: external_exports3.boolean(),
|
|
35768
35868
|
checks: external_exports3.object({
|
|
@@ -35773,6 +35873,7 @@ var IssueVettingResultSchema = external_exports3.object({
|
|
|
35773
35873
|
contributionGuidelinesFound: external_exports3.boolean()
|
|
35774
35874
|
}),
|
|
35775
35875
|
contributionGuidelines: ContributionGuidelinesSchema.optional(),
|
|
35876
|
+
linkedPR: LinkedPRSchema.nullable().optional(),
|
|
35776
35877
|
notes: external_exports3.array(external_exports3.string())
|
|
35777
35878
|
});
|
|
35778
35879
|
var TrackedIssueSchema = external_exports3.object({
|
|
@@ -35795,6 +35896,7 @@ var SkippedIssueSchema = external_exports3.object({
|
|
|
35795
35896
|
title: external_exports3.string(),
|
|
35796
35897
|
skippedAt: external_exports3.string()
|
|
35797
35898
|
});
|
|
35899
|
+
var HorizonSchema = external_exports3.enum(["quick-win", "bigger-bet"]);
|
|
35798
35900
|
var SavedCandidateSchema = external_exports3.object({
|
|
35799
35901
|
issueUrl: external_exports3.string(),
|
|
35800
35902
|
repo: external_exports3.string(),
|
|
@@ -35806,7 +35908,8 @@ var SavedCandidateSchema = external_exports3.object({
|
|
|
35806
35908
|
searchPriority: external_exports3.string(),
|
|
35807
35909
|
firstSeenAt: external_exports3.string(),
|
|
35808
35910
|
lastSeenAt: external_exports3.string(),
|
|
35809
|
-
lastScore: external_exports3.number()
|
|
35911
|
+
lastScore: external_exports3.number(),
|
|
35912
|
+
horizon: HorizonSchema.optional()
|
|
35810
35913
|
});
|
|
35811
35914
|
var PersistenceModeSchema = external_exports3.enum(["local", "gist"]);
|
|
35812
35915
|
var ScoutPreferencesSchema = external_exports3.object({
|
|
@@ -35826,7 +35929,32 @@ var ScoutPreferencesSchema = external_exports3.object({
|
|
|
35826
35929
|
persistence: PersistenceModeSchema.default("local"),
|
|
35827
35930
|
defaultStrategy: external_exports3.array(SearchStrategySchema).optional(),
|
|
35828
35931
|
broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
|
|
35829
|
-
skipBroadWhenSufficientResults: external_exports3.number().int().min(0).max(100).default(15)
|
|
35932
|
+
skipBroadWhenSufficientResults: external_exports3.number().int().min(0).max(100).default(15),
|
|
35933
|
+
/**
|
|
35934
|
+
* Optional Ollama model id used for SLM pre-triage during vetting
|
|
35935
|
+
* (oss-autopilot#1122). Empty disables the feature. Recommended values:
|
|
35936
|
+
* `gemma4:e4b` (default for capable hardware) or `gemma4:e2b` /
|
|
35937
|
+
* `qwen3:1.7b` for low-RAM machines.
|
|
35938
|
+
*/
|
|
35939
|
+
slmTriageModel: external_exports3.string().default(""),
|
|
35940
|
+
/**
|
|
35941
|
+
* Override the Ollama HTTP host. Defaults to `http://127.0.0.1:11434`
|
|
35942
|
+
* when empty. Useful when Ollama runs on a different machine on the
|
|
35943
|
+
* local network.
|
|
35944
|
+
*/
|
|
35945
|
+
slmTriageHost: external_exports3.string().default(""),
|
|
35946
|
+
/**
|
|
35947
|
+
* Minimum merged-PR count for a repo to qualify as an anchor in
|
|
35948
|
+
* `scout features` (#98). Lowering surfaces more anchors at the cost
|
|
35949
|
+
* of weaker prior engagement signal.
|
|
35950
|
+
*/
|
|
35951
|
+
featuresAnchorThreshold: external_exports3.number().int().min(1).max(50).default(3),
|
|
35952
|
+
/**
|
|
35953
|
+
* Quick-wins / bigger-bets split ratio for `scout features` (#99).
|
|
35954
|
+
* 0.6 means 60% quick wins, 40% bigger bets when both pools are
|
|
35955
|
+
* abundant. Deficits redirect to the other bucket.
|
|
35956
|
+
*/
|
|
35957
|
+
featuresSplitRatio: external_exports3.number().min(0).max(1).default(0.6)
|
|
35830
35958
|
});
|
|
35831
35959
|
var ScoutStateSchema = external_exports3.object({
|
|
35832
35960
|
version: external_exports3.literal(1),
|
|
@@ -35972,6 +36100,16 @@ function calculateViabilityScore(params) {
|
|
|
35972
36100
|
if (params.closedWithoutMergeCount > 0 && params.mergedPRCount === 0) {
|
|
35973
36101
|
score -= 15;
|
|
35974
36102
|
}
|
|
36103
|
+
if (params.featureSignals) {
|
|
36104
|
+
const fs5 = params.featureSignals;
|
|
36105
|
+
score += Math.min(Math.floor(fs5.reactions / 2), 10);
|
|
36106
|
+
if (fs5.comments >= 5)
|
|
36107
|
+
score += 5;
|
|
36108
|
+
if (fs5.hasMilestone)
|
|
36109
|
+
score += 5;
|
|
36110
|
+
if (fs5.wontfixNoContributor)
|
|
36111
|
+
score += 10;
|
|
36112
|
+
}
|
|
35975
36113
|
return Math.max(0, Math.min(100, score));
|
|
35976
36114
|
}
|
|
35977
36115
|
|
|
@@ -36363,6 +36501,35 @@ function isNotModifiedError(err) {
|
|
|
36363
36501
|
}
|
|
36364
36502
|
|
|
36365
36503
|
// ../core/dist/core/issue-eligibility.js
|
|
36504
|
+
function isLinkedPREvent(e) {
|
|
36505
|
+
return e.event === "cross-referenced" && !!e.source?.issue?.pull_request;
|
|
36506
|
+
}
|
|
36507
|
+
function buildLinkedPRFromTimelineEvent(e, context) {
|
|
36508
|
+
const issue2 = e.source?.issue;
|
|
36509
|
+
const ctx = `${context.owner}/${context.repo}#${context.issueNumber}`;
|
|
36510
|
+
if (!issue2 || typeof issue2.number !== "number") {
|
|
36511
|
+
warn(MODULE5, `Cross-referenced timeline event for ${ctx} missing source.issue.number \u2014 possible API shape drift`);
|
|
36512
|
+
return null;
|
|
36513
|
+
}
|
|
36514
|
+
const author = issue2.user?.login;
|
|
36515
|
+
if (!author) {
|
|
36516
|
+
warn(MODULE5, `Cross-referenced PR #${issue2.number} for ${ctx} has no user.login (deleted user?) \u2014 skipping linkedPR metadata`);
|
|
36517
|
+
return null;
|
|
36518
|
+
}
|
|
36519
|
+
const url2 = issue2.html_url;
|
|
36520
|
+
if (!url2) {
|
|
36521
|
+
warn(MODULE5, `Cross-referenced PR #${issue2.number} for ${ctx} missing html_url \u2014 skipping linkedPR metadata`);
|
|
36522
|
+
return null;
|
|
36523
|
+
}
|
|
36524
|
+
return {
|
|
36525
|
+
number: issue2.number,
|
|
36526
|
+
author,
|
|
36527
|
+
state: issue2.state === "closed" ? "closed" : "open",
|
|
36528
|
+
merged: !!issue2.pull_request?.merged_at,
|
|
36529
|
+
url: url2,
|
|
36530
|
+
updatedAt: issue2.updated_at
|
|
36531
|
+
};
|
|
36532
|
+
}
|
|
36366
36533
|
var MODULE5 = "issue-eligibility";
|
|
36367
36534
|
var CLAIM_PHRASES = [
|
|
36368
36535
|
"i'm working on this",
|
|
@@ -36390,15 +36557,27 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
|
36390
36557
|
per_page: 100,
|
|
36391
36558
|
page
|
|
36392
36559
|
}));
|
|
36393
|
-
|
|
36560
|
+
let linkedPRCount = 0;
|
|
36561
|
+
let linkedPR = null;
|
|
36562
|
+
for (const event of timeline) {
|
|
36394
36563
|
const e = event;
|
|
36395
|
-
|
|
36396
|
-
|
|
36397
|
-
|
|
36564
|
+
if (!isLinkedPREvent(e))
|
|
36565
|
+
continue;
|
|
36566
|
+
linkedPRCount++;
|
|
36567
|
+
linkedPR ??= buildLinkedPRFromTimelineEvent(e, {
|
|
36568
|
+
owner,
|
|
36569
|
+
repo,
|
|
36570
|
+
issueNumber
|
|
36571
|
+
});
|
|
36572
|
+
}
|
|
36573
|
+
return { passed: linkedPRCount === 0, linkedPR };
|
|
36398
36574
|
} catch (error48) {
|
|
36575
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36576
|
+
throw error48;
|
|
36577
|
+
}
|
|
36399
36578
|
const errMsg = errorMessage(error48);
|
|
36400
36579
|
warn(MODULE5, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
36401
|
-
return { passed: true, inconclusive: true, reason: errMsg };
|
|
36580
|
+
return { passed: true, inconclusive: true, reason: errMsg, linkedPR: null };
|
|
36402
36581
|
}
|
|
36403
36582
|
}
|
|
36404
36583
|
var MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
@@ -36424,6 +36603,9 @@ async function checkUserMergedPRsInRepo(octokit, owner, repo) {
|
|
|
36424
36603
|
tracker.recordCall();
|
|
36425
36604
|
}
|
|
36426
36605
|
} catch (error48) {
|
|
36606
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36607
|
+
throw error48;
|
|
36608
|
+
}
|
|
36427
36609
|
const errMsg = errorMessage(error48);
|
|
36428
36610
|
warn(MODULE5, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
36429
36611
|
return 0;
|
|
@@ -36448,6 +36630,9 @@ async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount)
|
|
|
36448
36630
|
}
|
|
36449
36631
|
return { passed: true };
|
|
36450
36632
|
} catch (error48) {
|
|
36633
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36634
|
+
throw error48;
|
|
36635
|
+
}
|
|
36451
36636
|
const errMsg = errorMessage(error48);
|
|
36452
36637
|
warn(MODULE5, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
36453
36638
|
return { passed: true, inconclusive: true, reason: errMsg };
|
|
@@ -36520,6 +36705,9 @@ async function checkProjectHealth(octokit, owner, repo) {
|
|
|
36520
36705
|
};
|
|
36521
36706
|
});
|
|
36522
36707
|
} catch (error48) {
|
|
36708
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36709
|
+
throw error48;
|
|
36710
|
+
}
|
|
36523
36711
|
const errMsg = errorMessage(error48);
|
|
36524
36712
|
warn(MODULE6, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
36525
36713
|
return {
|
|
@@ -36553,6 +36741,13 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
36553
36741
|
}
|
|
36554
36742
|
return null;
|
|
36555
36743
|
})));
|
|
36744
|
+
for (const result of results) {
|
|
36745
|
+
if (result.status !== "rejected")
|
|
36746
|
+
continue;
|
|
36747
|
+
if (getHttpStatusCode(result.reason) === 401 || isRateLimitError(result.reason)) {
|
|
36748
|
+
throw result.reason;
|
|
36749
|
+
}
|
|
36750
|
+
}
|
|
36556
36751
|
for (let i = 0; i < results.length; i++) {
|
|
36557
36752
|
const result = results[i];
|
|
36558
36753
|
if (result.status === "fulfilled" && result.value) {
|
|
@@ -36562,9 +36757,9 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
36562
36757
|
return guidelines;
|
|
36563
36758
|
}
|
|
36564
36759
|
if (result.status === "rejected") {
|
|
36565
|
-
const
|
|
36566
|
-
if (
|
|
36567
|
-
warn(MODULE6, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${
|
|
36760
|
+
const status = getHttpStatusCode(result.reason);
|
|
36761
|
+
if (status !== 404) {
|
|
36762
|
+
warn(MODULE6, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${errorMessage(result.reason)}`);
|
|
36568
36763
|
}
|
|
36569
36764
|
}
|
|
36570
36765
|
}
|
|
@@ -36614,8 +36809,274 @@ function parseContributionGuidelines(content) {
|
|
|
36614
36809
|
return guidelines;
|
|
36615
36810
|
}
|
|
36616
36811
|
|
|
36812
|
+
// ../core/dist/core/anti-llm-policy.js
|
|
36813
|
+
var MODULE7 = "anti-llm-policy";
|
|
36814
|
+
var POLICY_SCAN_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
36815
|
+
var ANTI_LLM_KEYWORDS = [
|
|
36816
|
+
"no ai-generated",
|
|
36817
|
+
"no ai generated",
|
|
36818
|
+
"no ai-assisted",
|
|
36819
|
+
"no ai assisted",
|
|
36820
|
+
"no llm-generated",
|
|
36821
|
+
"no llm generated",
|
|
36822
|
+
"no copilot-generated",
|
|
36823
|
+
"no chatgpt-generated",
|
|
36824
|
+
"human-authored only",
|
|
36825
|
+
"human authored only",
|
|
36826
|
+
"human-written only",
|
|
36827
|
+
"human written only",
|
|
36828
|
+
"ai-free contributions",
|
|
36829
|
+
"llm-free contributions",
|
|
36830
|
+
"ai-generated code is not allowed",
|
|
36831
|
+
"ai-generated code will not be accepted",
|
|
36832
|
+
"do not submit ai-generated",
|
|
36833
|
+
"do not submit llm-generated",
|
|
36834
|
+
"do not use ai to",
|
|
36835
|
+
"do not use llms",
|
|
36836
|
+
"do not use copilot",
|
|
36837
|
+
"do not use chatgpt",
|
|
36838
|
+
"without ai assistance",
|
|
36839
|
+
"without llm assistance",
|
|
36840
|
+
"no use of generative ai",
|
|
36841
|
+
"ban on ai-generated",
|
|
36842
|
+
"prohibit ai-generated",
|
|
36843
|
+
"prohibits ai-generated"
|
|
36844
|
+
];
|
|
36845
|
+
function scanForAntiLLMPolicy(text) {
|
|
36846
|
+
if (!text)
|
|
36847
|
+
return { matched: false, matchedKeywords: [] };
|
|
36848
|
+
const haystack = text.toLowerCase();
|
|
36849
|
+
const matchedKeywords = ANTI_LLM_KEYWORDS.filter((kw) => haystack.includes(kw));
|
|
36850
|
+
return { matched: matchedKeywords.length > 0, matchedKeywords };
|
|
36851
|
+
}
|
|
36852
|
+
var SOURCE_FILE_FAMILIES = [
|
|
36853
|
+
{
|
|
36854
|
+
canonical: "CONTRIBUTING.md",
|
|
36855
|
+
paths: [
|
|
36856
|
+
"CONTRIBUTING.md",
|
|
36857
|
+
".github/CONTRIBUTING.md",
|
|
36858
|
+
"docs/CONTRIBUTING.md",
|
|
36859
|
+
"contributing.md"
|
|
36860
|
+
]
|
|
36861
|
+
},
|
|
36862
|
+
{
|
|
36863
|
+
canonical: "CODE_OF_CONDUCT.md",
|
|
36864
|
+
paths: [
|
|
36865
|
+
"CODE_OF_CONDUCT.md",
|
|
36866
|
+
".github/CODE_OF_CONDUCT.md",
|
|
36867
|
+
"docs/CODE_OF_CONDUCT.md",
|
|
36868
|
+
"code_of_conduct.md"
|
|
36869
|
+
]
|
|
36870
|
+
},
|
|
36871
|
+
{
|
|
36872
|
+
canonical: "README.md",
|
|
36873
|
+
paths: ["README.md", "readme.md", "Readme.md"]
|
|
36874
|
+
}
|
|
36875
|
+
];
|
|
36876
|
+
async function fetchFileText(octokit, owner, repo, path5) {
|
|
36877
|
+
try {
|
|
36878
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
|
|
36879
|
+
if ("content" in data && typeof data.content === "string") {
|
|
36880
|
+
return {
|
|
36881
|
+
text: Buffer.from(data.content, "base64").toString("utf-8"),
|
|
36882
|
+
transient: false
|
|
36883
|
+
};
|
|
36884
|
+
}
|
|
36885
|
+
return { text: null, transient: false };
|
|
36886
|
+
} catch (error48) {
|
|
36887
|
+
const status = getHttpStatusCode(error48);
|
|
36888
|
+
if (status === 404)
|
|
36889
|
+
return { text: null, transient: false };
|
|
36890
|
+
if (status === 401 || isRateLimitError(error48))
|
|
36891
|
+
throw error48;
|
|
36892
|
+
warn(MODULE7, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
|
|
36893
|
+
return { text: null, transient: true };
|
|
36894
|
+
}
|
|
36895
|
+
}
|
|
36896
|
+
async function fetchFamilyText(octokit, owner, repo, paths) {
|
|
36897
|
+
const results = await Promise.allSettled(paths.map((p) => fetchFileText(octokit, owner, repo, p)));
|
|
36898
|
+
let hadTransientFailure = false;
|
|
36899
|
+
for (const result of results) {
|
|
36900
|
+
if (result.status === "fulfilled") {
|
|
36901
|
+
if (result.value.transient)
|
|
36902
|
+
hadTransientFailure = true;
|
|
36903
|
+
if (result.value.text)
|
|
36904
|
+
return { text: result.value.text, hadTransientFailure };
|
|
36905
|
+
} else {
|
|
36906
|
+
if (isRateLimitError(result.reason) || getHttpStatusCode(result.reason) === 401) {
|
|
36907
|
+
throw result.reason;
|
|
36908
|
+
}
|
|
36909
|
+
hadTransientFailure = true;
|
|
36910
|
+
}
|
|
36911
|
+
}
|
|
36912
|
+
return { text: null, hadTransientFailure };
|
|
36913
|
+
}
|
|
36914
|
+
function isAntiLLMPolicyResult(value) {
|
|
36915
|
+
if (!value || typeof value !== "object")
|
|
36916
|
+
return false;
|
|
36917
|
+
const v = value;
|
|
36918
|
+
if (typeof v.matched !== "boolean")
|
|
36919
|
+
return false;
|
|
36920
|
+
if (!Array.isArray(v.matchedKeywords))
|
|
36921
|
+
return false;
|
|
36922
|
+
if (v.sourceFile !== null && typeof v.sourceFile !== "string")
|
|
36923
|
+
return false;
|
|
36924
|
+
return true;
|
|
36925
|
+
}
|
|
36926
|
+
async function fetchAndScanAntiLLMPolicy(octokit, owner, repo, options) {
|
|
36927
|
+
const cache = getHttpCache();
|
|
36928
|
+
const cacheKey = `anti-llm-policy:${owner}/${repo}`;
|
|
36929
|
+
const cached2 = cache.getIfFresh(cacheKey, POLICY_SCAN_CACHE_TTL_MS);
|
|
36930
|
+
if (isAntiLLMPolicyResult(cached2))
|
|
36931
|
+
return cached2;
|
|
36932
|
+
let anyTransientFailure = false;
|
|
36933
|
+
for (const family of SOURCE_FILE_FAMILIES) {
|
|
36934
|
+
let text;
|
|
36935
|
+
let hadTransientFailure = false;
|
|
36936
|
+
if (family.canonical === "CONTRIBUTING.md" && options?.contributingText !== void 0) {
|
|
36937
|
+
text = options.contributingText;
|
|
36938
|
+
} else {
|
|
36939
|
+
({ text, hadTransientFailure } = await fetchFamilyText(octokit, owner, repo, family.paths));
|
|
36940
|
+
}
|
|
36941
|
+
if (hadTransientFailure)
|
|
36942
|
+
anyTransientFailure = true;
|
|
36943
|
+
if (!text)
|
|
36944
|
+
continue;
|
|
36945
|
+
const { matched, matchedKeywords } = scanForAntiLLMPolicy(text);
|
|
36946
|
+
if (matched) {
|
|
36947
|
+
const result = {
|
|
36948
|
+
matched: true,
|
|
36949
|
+
matchedKeywords,
|
|
36950
|
+
sourceFile: family.canonical
|
|
36951
|
+
};
|
|
36952
|
+
cache.set(cacheKey, "", result);
|
|
36953
|
+
return result;
|
|
36954
|
+
}
|
|
36955
|
+
}
|
|
36956
|
+
const noMatch = {
|
|
36957
|
+
matched: false,
|
|
36958
|
+
matchedKeywords: [],
|
|
36959
|
+
sourceFile: null
|
|
36960
|
+
};
|
|
36961
|
+
if (!anyTransientFailure) {
|
|
36962
|
+
cache.set(cacheKey, "", noMatch);
|
|
36963
|
+
}
|
|
36964
|
+
return noMatch;
|
|
36965
|
+
}
|
|
36966
|
+
|
|
36967
|
+
// ../core/dist/core/slm-triage.js
|
|
36968
|
+
var DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
|
|
36969
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
36970
|
+
var MAX_BODY_CHARS = 2e3;
|
|
36971
|
+
var SLM_TRIAGE_SCHEMA = {
|
|
36972
|
+
type: "object",
|
|
36973
|
+
properties: {
|
|
36974
|
+
decision: { type: "string", enum: ["pursue", "investigate", "skip"] },
|
|
36975
|
+
confidence: { type: "string", enum: ["high", "medium", "low"] },
|
|
36976
|
+
reasons: {
|
|
36977
|
+
type: "array",
|
|
36978
|
+
items: { type: "string" },
|
|
36979
|
+
minItems: 1,
|
|
36980
|
+
maxItems: 3
|
|
36981
|
+
}
|
|
36982
|
+
},
|
|
36983
|
+
required: ["decision", "confidence", "reasons"]
|
|
36984
|
+
};
|
|
36985
|
+
function buildPrompt(input) {
|
|
36986
|
+
const { issue: issue2, linkedPRExists } = input;
|
|
36987
|
+
const body = (issue2.body ?? "").slice(0, MAX_BODY_CHARS);
|
|
36988
|
+
return [
|
|
36989
|
+
"You triage open-source issues for an autonomous contribution agent.",
|
|
36990
|
+
"Classify the issue into exactly one bucket:",
|
|
36991
|
+
"- pursue: small, concrete bug or feature with clear acceptance; safe for an autonomous agent to attempt without further design input",
|
|
36992
|
+
"- investigate: tractable but needs human reading first (ambiguous scope, design questions, recently-touched files)",
|
|
36993
|
+
"- skip: not actionable autonomously (epic, creative, blocked, requires upstream change, requires infra)",
|
|
36994
|
+
"",
|
|
36995
|
+
"Return JSON only matching the provided schema. Reasons must be short phrases, not sentences. 1-3 reasons total.",
|
|
36996
|
+
"",
|
|
36997
|
+
"Issue:",
|
|
36998
|
+
`Title: ${issue2.title}`,
|
|
36999
|
+
`Body: ${body}`,
|
|
37000
|
+
`Labels: ${issue2.labels.join(", ")}`,
|
|
37001
|
+
`Linked PR exists: ${linkedPRExists}`
|
|
37002
|
+
].join("\n");
|
|
37003
|
+
}
|
|
37004
|
+
async function triageWithSLM(input, options) {
|
|
37005
|
+
if (!options.model)
|
|
37006
|
+
return null;
|
|
37007
|
+
const host = options.host ?? DEFAULT_OLLAMA_HOST;
|
|
37008
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
37009
|
+
const fetchFn = options.fetchImpl ?? fetch;
|
|
37010
|
+
let response;
|
|
37011
|
+
try {
|
|
37012
|
+
response = await fetchFn(`${host}/api/chat`, {
|
|
37013
|
+
method: "POST",
|
|
37014
|
+
headers: { "Content-Type": "application/json" },
|
|
37015
|
+
body: JSON.stringify({
|
|
37016
|
+
model: options.model,
|
|
37017
|
+
messages: [{ role: "user", content: buildPrompt(input) }],
|
|
37018
|
+
stream: false,
|
|
37019
|
+
format: SLM_TRIAGE_SCHEMA,
|
|
37020
|
+
options: { temperature: 0.1 }
|
|
37021
|
+
}),
|
|
37022
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
37023
|
+
});
|
|
37024
|
+
} catch {
|
|
37025
|
+
return null;
|
|
37026
|
+
}
|
|
37027
|
+
if (!response.ok)
|
|
37028
|
+
return null;
|
|
37029
|
+
let payload;
|
|
37030
|
+
try {
|
|
37031
|
+
payload = await response.json();
|
|
37032
|
+
} catch {
|
|
37033
|
+
return null;
|
|
37034
|
+
}
|
|
37035
|
+
const content = payload?.message?.content;
|
|
37036
|
+
if (typeof content !== "string")
|
|
37037
|
+
return null;
|
|
37038
|
+
let parsed;
|
|
37039
|
+
try {
|
|
37040
|
+
parsed = JSON.parse(content);
|
|
37041
|
+
} catch {
|
|
37042
|
+
return null;
|
|
37043
|
+
}
|
|
37044
|
+
if (!isValidTriageShape(parsed))
|
|
37045
|
+
return null;
|
|
37046
|
+
return {
|
|
37047
|
+
decision: parsed.decision,
|
|
37048
|
+
confidence: parsed.confidence,
|
|
37049
|
+
reasons: parsed.reasons,
|
|
37050
|
+
modelVersion: options.model
|
|
37051
|
+
};
|
|
37052
|
+
}
|
|
37053
|
+
function buildTriageInput(args) {
|
|
37054
|
+
return {
|
|
37055
|
+
issue: {
|
|
37056
|
+
title: args.issue.title,
|
|
37057
|
+
labels: args.issue.labels,
|
|
37058
|
+
body: args.issue.body
|
|
37059
|
+
},
|
|
37060
|
+
linkedPRExists: !!args.linkedPR
|
|
37061
|
+
};
|
|
37062
|
+
}
|
|
37063
|
+
function isValidTriageShape(value) {
|
|
37064
|
+
if (typeof value !== "object" || value === null)
|
|
37065
|
+
return false;
|
|
37066
|
+
const v = value;
|
|
37067
|
+
if (v.decision !== "pursue" && v.decision !== "investigate" && v.decision !== "skip")
|
|
37068
|
+
return false;
|
|
37069
|
+
if (v.confidence !== "high" && v.confidence !== "medium" && v.confidence !== "low")
|
|
37070
|
+
return false;
|
|
37071
|
+
if (!Array.isArray(v.reasons) || v.reasons.length === 0 || v.reasons.length > 3)
|
|
37072
|
+
return false;
|
|
37073
|
+
if (!v.reasons.every((r) => typeof r === "string"))
|
|
37074
|
+
return false;
|
|
37075
|
+
return true;
|
|
37076
|
+
}
|
|
37077
|
+
|
|
36617
37078
|
// ../core/dist/core/issue-vetting.js
|
|
36618
|
-
var
|
|
37079
|
+
var MODULE8 = "issue-vetting";
|
|
36619
37080
|
var MAX_CONCURRENT_VETTING = 3;
|
|
36620
37081
|
var VETTING_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
36621
37082
|
var IssueVetter = class {
|
|
@@ -36628,13 +37089,19 @@ var IssueVetter = class {
|
|
|
36628
37089
|
/**
|
|
36629
37090
|
* Vet a specific issue — runs all checks and computes recommendation + viability score.
|
|
36630
37091
|
* Results are cached for 15 minutes to avoid redundant API calls on repeated searches.
|
|
37092
|
+
*
|
|
37093
|
+
* `opts.featureSignals` are forwarded directly to scoring; the vetter does
|
|
37094
|
+
* not derive them from the fetched issue. Cache key includes a digest of
|
|
37095
|
+
* the signals so the same URL with different signals doesn't return a
|
|
37096
|
+
* stale score.
|
|
36631
37097
|
*/
|
|
36632
|
-
async vetIssue(issueUrl) {
|
|
37098
|
+
async vetIssue(issueUrl, opts) {
|
|
36633
37099
|
const cache = getHttpCache();
|
|
36634
|
-
const
|
|
37100
|
+
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}`;
|
|
36635
37102
|
const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
|
|
36636
37103
|
if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
|
|
36637
|
-
debug(
|
|
37104
|
+
debug(MODULE8, `Vetting cache hit for ${issueUrl}`);
|
|
36638
37105
|
return cached2;
|
|
36639
37106
|
}
|
|
36640
37107
|
const parsed = parseGitHubUrl(issueUrl);
|
|
@@ -36657,6 +37124,7 @@ var IssueVetter = class {
|
|
|
36657
37124
|
fetchContributionGuidelines(this.octokit, owner, repo),
|
|
36658
37125
|
hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo)
|
|
36659
37126
|
]);
|
|
37127
|
+
const antiLLMPolicy = await fetchAndScanAntiLLMPolicy(this.octokit, owner, repo, { contributingText: contributionGuidelines?.rawContent });
|
|
36660
37128
|
const noExistingPR = existingPRCheck.passed;
|
|
36661
37129
|
const notClaimed = claimCheck.passed;
|
|
36662
37130
|
const clearRequirements = analyzeRequirements(ghIssue.body || "");
|
|
@@ -36671,6 +37139,7 @@ var IssueVetter = class {
|
|
|
36671
37139
|
contributionGuidelinesFound: !!contributionGuidelines
|
|
36672
37140
|
},
|
|
36673
37141
|
contributionGuidelines,
|
|
37142
|
+
linkedPR: existingPRCheck.linkedPR,
|
|
36674
37143
|
notes: []
|
|
36675
37144
|
};
|
|
36676
37145
|
if (!noExistingPR)
|
|
@@ -36771,7 +37240,8 @@ var IssueVetter = class {
|
|
|
36771
37240
|
mergedPRCount: effectiveMergedCount,
|
|
36772
37241
|
orgHasMergedPRs,
|
|
36773
37242
|
repoQualityBonus,
|
|
36774
|
-
matchesPreferredCategory: matchesCategory
|
|
37243
|
+
matchesPreferredCategory: matchesCategory,
|
|
37244
|
+
featureSignals: opts?.featureSignals
|
|
36775
37245
|
});
|
|
36776
37246
|
const starredRepos = this.stateReader.getStarredRepos();
|
|
36777
37247
|
let searchPriority = "normal";
|
|
@@ -36780,10 +37250,26 @@ var IssueVetter = class {
|
|
|
36780
37250
|
} else if (starredRepos.includes(repoFullName)) {
|
|
36781
37251
|
searchPriority = "starred";
|
|
36782
37252
|
}
|
|
37253
|
+
const slmConfig = this.stateReader.getSLMTriageConfig?.() ?? {
|
|
37254
|
+
model: "",
|
|
37255
|
+
host: ""
|
|
37256
|
+
};
|
|
37257
|
+
let slmTriage = null;
|
|
37258
|
+
if (slmConfig.model) {
|
|
37259
|
+
const slmOpts = { model: slmConfig.model };
|
|
37260
|
+
if (slmConfig.host)
|
|
37261
|
+
slmOpts.host = slmConfig.host;
|
|
37262
|
+
slmTriage = await triageWithSLM(buildTriageInput({
|
|
37263
|
+
issue: { ...trackedIssue, body: ghIssue.body ?? "" },
|
|
37264
|
+
linkedPR: existingPRCheck.linkedPR ?? null
|
|
37265
|
+
}), slmOpts);
|
|
37266
|
+
}
|
|
36783
37267
|
const result = {
|
|
36784
37268
|
issue: trackedIssue,
|
|
36785
37269
|
vettingResult,
|
|
36786
37270
|
projectHealth,
|
|
37271
|
+
antiLLMPolicy,
|
|
37272
|
+
slmTriage,
|
|
36787
37273
|
recommendation,
|
|
36788
37274
|
reasonsToSkip,
|
|
36789
37275
|
reasonsToApprove,
|
|
@@ -36802,9 +37288,12 @@ var IssueVetter = class {
|
|
|
36802
37288
|
let failedVettingCount = 0;
|
|
36803
37289
|
let rateLimitFailures = 0;
|
|
36804
37290
|
let attemptedCount = 0;
|
|
37291
|
+
let firstAuthError = null;
|
|
36805
37292
|
for (const url2 of urls) {
|
|
36806
37293
|
if (candidates.length >= maxResults)
|
|
36807
37294
|
break;
|
|
37295
|
+
if (firstAuthError)
|
|
37296
|
+
break;
|
|
36808
37297
|
attemptedCount++;
|
|
36809
37298
|
const task = this.vetIssue(url2).then((candidate) => {
|
|
36810
37299
|
if (candidates.length < maxResults) {
|
|
@@ -36814,11 +37303,15 @@ var IssueVetter = class {
|
|
|
36814
37303
|
candidates.push(candidate);
|
|
36815
37304
|
}
|
|
36816
37305
|
}).catch((error48) => {
|
|
37306
|
+
if (getHttpStatusCode(error48) === 401) {
|
|
37307
|
+
firstAuthError ??= error48;
|
|
37308
|
+
return;
|
|
37309
|
+
}
|
|
36817
37310
|
failedVettingCount++;
|
|
36818
37311
|
if (isRateLimitError(error48)) {
|
|
36819
37312
|
rateLimitFailures++;
|
|
36820
37313
|
}
|
|
36821
|
-
warn(
|
|
37314
|
+
warn(MODULE8, `Error vetting issue ${url2}:`, errorMessage(error48));
|
|
36822
37315
|
}).finally(() => pending.delete(url2));
|
|
36823
37316
|
pending.set(url2, task);
|
|
36824
37317
|
if (pending.size >= MAX_CONCURRENT_VETTING) {
|
|
@@ -36826,9 +37319,15 @@ var IssueVetter = class {
|
|
|
36826
37319
|
}
|
|
36827
37320
|
}
|
|
36828
37321
|
await Promise.allSettled(pending.values());
|
|
37322
|
+
if (firstAuthError) {
|
|
37323
|
+
if (candidates.length > 0) {
|
|
37324
|
+
warn(MODULE8, `Auth failed mid-batch after ${candidates.length} successful vet(s) \u2014 discarding partial results`);
|
|
37325
|
+
}
|
|
37326
|
+
throw firstAuthError;
|
|
37327
|
+
}
|
|
36829
37328
|
const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
|
|
36830
37329
|
if (allFailed) {
|
|
36831
|
-
warn(
|
|
37330
|
+
warn(MODULE8, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
36832
37331
|
}
|
|
36833
37332
|
return {
|
|
36834
37333
|
candidates: candidates.slice(0, maxResults),
|
|
@@ -36839,14 +37338,14 @@ var IssueVetter = class {
|
|
|
36839
37338
|
};
|
|
36840
37339
|
|
|
36841
37340
|
// ../core/dist/core/search-phases.js
|
|
36842
|
-
var
|
|
37341
|
+
var MODULE9 = "search-phases";
|
|
36843
37342
|
var GITHUB_MAX_BOOLEAN_OPS = 5;
|
|
36844
37343
|
var INTER_QUERY_DELAY_MS = 2e3;
|
|
36845
37344
|
function chunkLabels(labels, reservedOps = 0) {
|
|
36846
37345
|
const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
|
|
36847
37346
|
if (maxPerChunk < 1) {
|
|
36848
37347
|
if (labels.length > 0) {
|
|
36849
|
-
warn(
|
|
37348
|
+
warn(MODULE9, `Label filtering disabled: ${reservedOps} repo/org ORs exceed GitHub's ${GITHUB_MAX_BOOLEAN_OPS} operator limit. All ${labels.length} label(s) dropped from query.`);
|
|
36850
37349
|
}
|
|
36851
37350
|
return [[]];
|
|
36852
37351
|
}
|
|
@@ -36856,7 +37355,7 @@ function chunkLabels(labels, reservedOps = 0) {
|
|
|
36856
37355
|
for (let i = 0; i < labels.length; i += maxPerChunk) {
|
|
36857
37356
|
chunks.push(labels.slice(i, i + maxPerChunk));
|
|
36858
37357
|
}
|
|
36859
|
-
debug(
|
|
37358
|
+
debug(MODULE9, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
|
|
36860
37359
|
return chunks;
|
|
36861
37360
|
}
|
|
36862
37361
|
function buildLabelQuery(labels) {
|
|
@@ -36893,7 +37392,7 @@ async function cachedSearchIssues(octokit, params) {
|
|
|
36893
37392
|
const cache = getHttpCache();
|
|
36894
37393
|
const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
|
|
36895
37394
|
if (cached2) {
|
|
36896
|
-
debug(
|
|
37395
|
+
debug(MODULE9, `Search cache hit for query`);
|
|
36897
37396
|
return cached2;
|
|
36898
37397
|
}
|
|
36899
37398
|
const tracker = getSearchBudgetTracker();
|
|
@@ -36908,7 +37407,7 @@ async function cachedSearchIssues(octokit, params) {
|
|
|
36908
37407
|
if (data.items.length > 0) {
|
|
36909
37408
|
cache.set(cacheKey, "", data);
|
|
36910
37409
|
} else {
|
|
36911
|
-
debug(
|
|
37410
|
+
debug(MODULE9, `Skipping cache for empty search result (possible rate limit artifact)`);
|
|
36912
37411
|
}
|
|
36913
37412
|
return data;
|
|
36914
37413
|
}
|
|
@@ -36956,10 +37455,10 @@ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResul
|
|
|
36956
37455
|
if (getHttpStatusCode(error48) === 401)
|
|
36957
37456
|
throw error48;
|
|
36958
37457
|
if (isRateLimitError(error48)) {
|
|
36959
|
-
warn(
|
|
37458
|
+
warn(MODULE9, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
36960
37459
|
break;
|
|
36961
37460
|
}
|
|
36962
|
-
warn(
|
|
37461
|
+
warn(MODULE9, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
36963
37462
|
}
|
|
36964
37463
|
}
|
|
36965
37464
|
return items;
|
|
@@ -37010,13 +37509,13 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
|
|
|
37010
37509
|
if (isRateLimitError(error48)) {
|
|
37011
37510
|
rateLimitFailures++;
|
|
37012
37511
|
}
|
|
37013
|
-
warn(
|
|
37512
|
+
warn(MODULE9, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37014
37513
|
}
|
|
37015
37514
|
}
|
|
37016
37515
|
const allReposFailed = failedRepos === repos.length && repos.length > 0;
|
|
37017
37516
|
const rateLimitHit = rateLimitFailures > 0;
|
|
37018
37517
|
if (allReposFailed) {
|
|
37019
|
-
warn(
|
|
37518
|
+
warn(MODULE9, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
37020
37519
|
}
|
|
37021
37520
|
return { candidates, allReposFailed, rateLimitHit };
|
|
37022
37521
|
}
|
|
@@ -37043,11 +37542,37 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
|
|
|
37043
37542
|
}
|
|
37044
37543
|
return allItems;
|
|
37045
37544
|
}
|
|
37545
|
+
function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
|
|
37546
|
+
if (isAnyLanguage || languages.length === 0)
|
|
37547
|
+
return [""];
|
|
37548
|
+
if (languages.length === 1)
|
|
37549
|
+
return [`language:${languages[0]}`];
|
|
37550
|
+
if (!hasLabels)
|
|
37551
|
+
return [languages.map((l) => `language:${l}`).join(" ")];
|
|
37552
|
+
return languages.map((l) => `language:${l}`);
|
|
37553
|
+
}
|
|
37554
|
+
async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage) {
|
|
37555
|
+
const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
|
|
37556
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
37557
|
+
const allItems = [];
|
|
37558
|
+
for (let i = 0; i < langVariants.length; i++) {
|
|
37559
|
+
if (i > 0)
|
|
37560
|
+
await sleep(INTER_QUERY_DELAY_MS);
|
|
37561
|
+
const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage);
|
|
37562
|
+
for (const item of items) {
|
|
37563
|
+
if (!seenUrls.has(item.html_url)) {
|
|
37564
|
+
seenUrls.add(item.html_url);
|
|
37565
|
+
allItems.push(item);
|
|
37566
|
+
}
|
|
37567
|
+
}
|
|
37568
|
+
}
|
|
37569
|
+
return allItems;
|
|
37570
|
+
}
|
|
37046
37571
|
async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
|
|
37047
37572
|
const spamRepos = detectLabelFarmingRepos(items);
|
|
37048
37573
|
if (spamRepos.size > 0) {
|
|
37049
37574
|
const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
|
|
37050
|
-
debug(
|
|
37575
|
+
debug(MODULE9, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
|
|
37051
37576
|
}
|
|
37052
37577
|
const itemsToVet = filterIssues(items).filter((item) => {
|
|
37053
37578
|
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
@@ -37058,7 +37583,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37058
37583
|
return excludedRepoSets.every((s) => !s.has(repoFullName));
|
|
37059
37584
|
}).slice(0, remainingNeeded * 2);
|
|
37060
37585
|
if (itemsToVet.length === 0) {
|
|
37061
|
-
debug(
|
|
37586
|
+
debug(MODULE9, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
|
|
37062
37587
|
return { candidates: [], allVetFailed: false, rateLimitHit: false };
|
|
37063
37588
|
}
|
|
37064
37589
|
const { candidates: results, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
|
|
@@ -37070,13 +37595,40 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37070
37595
|
});
|
|
37071
37596
|
const starFilteredCount = results.length - starFiltered.length;
|
|
37072
37597
|
if (starFilteredCount > 0) {
|
|
37073
|
-
debug(
|
|
37598
|
+
debug(MODULE9, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
|
|
37074
37599
|
}
|
|
37075
37600
|
return { candidates: starFiltered, allVetFailed, rateLimitHit };
|
|
37076
37601
|
}
|
|
37077
37602
|
|
|
37603
|
+
// ../core/dist/core/personalization.js
|
|
37604
|
+
var REPO_BOOST = 20;
|
|
37605
|
+
var LANGUAGE_BOOST = 10;
|
|
37606
|
+
function annotateBoost(candidates, preferLanguages, preferRepos) {
|
|
37607
|
+
const langSet = new Set((preferLanguages ?? []).map((l) => l.trim().toLowerCase()).filter(Boolean));
|
|
37608
|
+
const repoSet = new Set((preferRepos ?? []).map((r) => r.trim()).filter(Boolean));
|
|
37609
|
+
if (langSet.size === 0 && repoSet.size === 0)
|
|
37610
|
+
return;
|
|
37611
|
+
for (const c of candidates) {
|
|
37612
|
+
let score = 0;
|
|
37613
|
+
const reasons = [];
|
|
37614
|
+
if (repoSet.size > 0 && repoSet.has(c.issue.repo)) {
|
|
37615
|
+
score += REPO_BOOST;
|
|
37616
|
+
reasons.push(`repo affinity: ${c.issue.repo}`);
|
|
37617
|
+
}
|
|
37618
|
+
const lang = c.projectHealth.language;
|
|
37619
|
+
if (langSet.size > 0 && lang && langSet.has(lang.toLowerCase())) {
|
|
37620
|
+
score += LANGUAGE_BOOST;
|
|
37621
|
+
reasons.push(`language match: ${lang}`);
|
|
37622
|
+
}
|
|
37623
|
+
if (score > 0) {
|
|
37624
|
+
c.boostScore = score;
|
|
37625
|
+
c.boostReasons = reasons;
|
|
37626
|
+
}
|
|
37627
|
+
}
|
|
37628
|
+
}
|
|
37629
|
+
|
|
37078
37630
|
// ../core/dist/core/issue-discovery.js
|
|
37079
|
-
var
|
|
37631
|
+
var MODULE10 = "issue-discovery";
|
|
37080
37632
|
var LOW_BUDGET_THRESHOLD = 20;
|
|
37081
37633
|
var CRITICAL_BUDGET_THRESHOLD = 10;
|
|
37082
37634
|
function buildIssueFilter(config2) {
|
|
@@ -37109,9 +37661,9 @@ function buildIssueFilter(config2) {
|
|
|
37109
37661
|
};
|
|
37110
37662
|
}
|
|
37111
37663
|
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
37112
|
-
info(
|
|
37664
|
+
info(MODULE10, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
37113
37665
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
37114
|
-
info(
|
|
37666
|
+
info(MODULE10, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
37115
37667
|
return {
|
|
37116
37668
|
candidates,
|
|
37117
37669
|
error: allReposFailed ? "All merged-PR repo fetches failed" : null,
|
|
@@ -37119,26 +37671,26 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
|
37119
37671
|
};
|
|
37120
37672
|
}
|
|
37121
37673
|
async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
|
|
37122
|
-
info(
|
|
37674
|
+
info(MODULE10, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
37123
37675
|
const phase1Labels = labels.slice(0, 3);
|
|
37124
37676
|
const reposToSearch = repos.slice(0, 10);
|
|
37125
37677
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
|
|
37126
|
-
info(
|
|
37678
|
+
info(MODULE10, `Found ${candidates.length} candidates from starred repos`);
|
|
37127
37679
|
return {
|
|
37128
37680
|
candidates,
|
|
37129
37681
|
error: allReposFailed ? "All starred repo fetches failed" : null,
|
|
37130
37682
|
rateLimitHit
|
|
37131
37683
|
};
|
|
37132
37684
|
}
|
|
37133
|
-
async function runPhase2(octokit, vetter, scopes, labels, configLabels,
|
|
37134
|
-
info(
|
|
37685
|
+
async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
37686
|
+
info(MODULE10, "Phase 2: General issue search...");
|
|
37135
37687
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37136
37688
|
const tierLabelGroups = [];
|
|
37137
37689
|
if (scopes && scopes.length > 1) {
|
|
37138
37690
|
for (const scope of scopes) {
|
|
37139
37691
|
const scopeLabels = SCOPE_LABELS[scope] ?? [];
|
|
37140
37692
|
if (scopeLabels.length === 0) {
|
|
37141
|
-
warn(
|
|
37693
|
+
warn(MODULE10, `Scope "${scope}" has no labels, skipping tier`);
|
|
37142
37694
|
continue;
|
|
37143
37695
|
}
|
|
37144
37696
|
tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
|
|
@@ -37157,8 +37709,8 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37157
37709
|
let rateLimitHit = false;
|
|
37158
37710
|
for (const { tier, tierLabels } of tierLabelGroups) {
|
|
37159
37711
|
try {
|
|
37160
|
-
const allItems = await
|
|
37161
|
-
info(
|
|
37712
|
+
const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
|
|
37713
|
+
info(MODULE10, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
|
|
37162
37714
|
const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
|
|
37163
37715
|
tierResults.push(tierCandidates);
|
|
37164
37716
|
for (const c of tierCandidates)
|
|
@@ -37169,7 +37721,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37169
37721
|
if (vetRateLimitHit) {
|
|
37170
37722
|
rateLimitHit = true;
|
|
37171
37723
|
}
|
|
37172
|
-
info(
|
|
37724
|
+
info(MODULE10, `Found ${tierCandidates.length} candidates from ${tier} tier`);
|
|
37173
37725
|
} catch (err) {
|
|
37174
37726
|
if (getHttpStatusCode(err) === 401)
|
|
37175
37727
|
throw err;
|
|
@@ -37178,13 +37730,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37178
37730
|
if (isRateLimitError(err)) {
|
|
37179
37731
|
rateLimitHit = true;
|
|
37180
37732
|
}
|
|
37181
|
-
warn(
|
|
37733
|
+
warn(MODULE10, `Error in ${tier} tier search: ${errMsg}`);
|
|
37182
37734
|
tierResults.push([]);
|
|
37183
37735
|
}
|
|
37184
37736
|
}
|
|
37185
37737
|
const interleaved = interleaveArrays(tierResults);
|
|
37186
37738
|
if (interleaved.length === 0 && error48) {
|
|
37187
|
-
warn(
|
|
37739
|
+
warn(MODULE10, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
|
|
37188
37740
|
}
|
|
37189
37741
|
return {
|
|
37190
37742
|
candidates: interleaved.slice(0, maxResults),
|
|
@@ -37193,17 +37745,17 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37193
37745
|
};
|
|
37194
37746
|
}
|
|
37195
37747
|
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
|
|
37196
|
-
info(
|
|
37748
|
+
info(MODULE10, "Phase 3: Searching actively maintained repos...");
|
|
37197
37749
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37198
37750
|
const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
|
|
37199
37751
|
if (eligibleStarred.length > 0) {
|
|
37200
|
-
info(
|
|
37752
|
+
info(MODULE10, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
|
|
37201
37753
|
const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
|
|
37202
37754
|
if (restItems.length > 0) {
|
|
37203
37755
|
try {
|
|
37204
37756
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
|
|
37205
37757
|
if (candidates.length > 0) {
|
|
37206
|
-
info(
|
|
37758
|
+
info(MODULE10, `Found ${candidates.length} candidates from maintained-repo REST search`);
|
|
37207
37759
|
return {
|
|
37208
37760
|
candidates,
|
|
37209
37761
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37213,11 +37765,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37213
37765
|
} catch (error48) {
|
|
37214
37766
|
if (getHttpStatusCode(error48) === 401)
|
|
37215
37767
|
throw error48;
|
|
37216
|
-
warn(
|
|
37768
|
+
warn(MODULE10, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
|
|
37217
37769
|
}
|
|
37218
37770
|
}
|
|
37219
37771
|
}
|
|
37220
|
-
info(
|
|
37772
|
+
info(MODULE10, "Phase 3: Falling back to Search API...");
|
|
37221
37773
|
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
37222
37774
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
37223
37775
|
const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
|
|
@@ -37231,17 +37783,19 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37231
37783
|
order: "desc",
|
|
37232
37784
|
per_page: maxResults * 3
|
|
37233
37785
|
});
|
|
37234
|
-
info(
|
|
37786
|
+
info(MODULE10, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
37235
37787
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
|
|
37236
|
-
info(
|
|
37788
|
+
info(MODULE10, `Found ${candidates.length} candidates from maintained-repo search`);
|
|
37237
37789
|
return {
|
|
37238
37790
|
candidates,
|
|
37239
37791
|
error: allVetFailed ? "all vetting failed" : null,
|
|
37240
37792
|
rateLimitHit: vetRateLimitHit
|
|
37241
37793
|
};
|
|
37242
37794
|
} catch (error48) {
|
|
37795
|
+
if (getHttpStatusCode(error48) === 401)
|
|
37796
|
+
throw error48;
|
|
37243
37797
|
const errMsg = errorMessage(error48);
|
|
37244
|
-
warn(
|
|
37798
|
+
warn(MODULE10, `Error in maintained-repo search: ${errMsg}`);
|
|
37245
37799
|
return {
|
|
37246
37800
|
candidates: [],
|
|
37247
37801
|
error: errMsg,
|
|
@@ -37325,18 +37879,18 @@ var IssueDiscovery = class {
|
|
|
37325
37879
|
if (rateLimit.remaining < 5) {
|
|
37326
37880
|
const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
|
|
37327
37881
|
this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
|
|
37328
|
-
warn(
|
|
37882
|
+
warn(MODULE10, this.rateLimitWarning);
|
|
37329
37883
|
}
|
|
37330
37884
|
if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
|
|
37331
|
-
info(
|
|
37885
|
+
info(MODULE10, `Search budget critical (${searchBudget} remaining) \u2014 running only Phase 0`);
|
|
37332
37886
|
} else if (searchBudget < LOW_BUDGET_THRESHOLD) {
|
|
37333
|
-
info(
|
|
37887
|
+
info(MODULE10, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
|
|
37334
37888
|
}
|
|
37335
37889
|
} catch (error48) {
|
|
37336
37890
|
if (getHttpStatusCode(error48) === 401)
|
|
37337
37891
|
throw error48;
|
|
37338
37892
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
|
|
37339
|
-
warn(
|
|
37893
|
+
warn(MODULE10, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
|
|
37340
37894
|
}
|
|
37341
37895
|
if (searchBudget <= 0) {
|
|
37342
37896
|
this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
@@ -37349,10 +37903,9 @@ var IssueDiscovery = class {
|
|
|
37349
37903
|
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
|
|
37350
37904
|
const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
|
|
37351
37905
|
const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
|
|
37352
|
-
const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`.replace(/ +/g, " ").trim();
|
|
37353
37906
|
const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
|
|
37354
37907
|
if (aiBlocklisted.size > 0) {
|
|
37355
|
-
debug(
|
|
37908
|
+
debug(MODULE10, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
|
|
37356
37909
|
}
|
|
37357
37910
|
const filterIssues = buildIssueFilter({
|
|
37358
37911
|
excludedRepos: new Set(config2.excludeRepos),
|
|
@@ -37388,7 +37941,7 @@ var IssueDiscovery = class {
|
|
|
37388
37941
|
}
|
|
37389
37942
|
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
|
|
37390
37943
|
if (interPhaseDelay > 0) {
|
|
37391
|
-
info(
|
|
37944
|
+
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37392
37945
|
await sleep(interPhaseDelay);
|
|
37393
37946
|
}
|
|
37394
37947
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
@@ -37408,20 +37961,20 @@ var IssueDiscovery = class {
|
|
|
37408
37961
|
const skipThreshold = config2.skipBroadWhenSufficientResults ?? 15;
|
|
37409
37962
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
|
|
37410
37963
|
if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
|
|
37411
|
-
info(
|
|
37964
|
+
info(MODULE10, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
|
|
37412
37965
|
} else {
|
|
37413
37966
|
if (interPhaseDelay > 0) {
|
|
37414
|
-
info(
|
|
37967
|
+
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37415
37968
|
await sleep(interPhaseDelay);
|
|
37416
37969
|
}
|
|
37417
37970
|
if (allCandidates.length > 0 && broadDelay > 0) {
|
|
37418
|
-
info(
|
|
37971
|
+
info(MODULE10, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
|
|
37419
37972
|
await sleep(broadDelay);
|
|
37420
37973
|
} else if (allCandidates.length === 0) {
|
|
37421
|
-
info(
|
|
37974
|
+
info(MODULE10, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
37422
37975
|
}
|
|
37423
37976
|
const remaining = maxResults - allCandidates.length;
|
|
37424
|
-
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels,
|
|
37977
|
+
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
37425
37978
|
allCandidates.push(...result.candidates);
|
|
37426
37979
|
phaseErrors["2"] = result.error;
|
|
37427
37980
|
if (result.rateLimitHit)
|
|
@@ -37431,7 +37984,7 @@ var IssueDiscovery = class {
|
|
|
37431
37984
|
}
|
|
37432
37985
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
|
|
37433
37986
|
if (interPhaseDelay > 0) {
|
|
37434
|
-
info(
|
|
37987
|
+
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37435
37988
|
await sleep(interPhaseDelay);
|
|
37436
37989
|
}
|
|
37437
37990
|
const remaining = maxResults - allCandidates.length;
|
|
@@ -37466,6 +38019,7 @@ var IssueDiscovery = class {
|
|
|
37466
38019
|
if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
|
|
37467
38020
|
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.`;
|
|
37468
38021
|
}
|
|
38022
|
+
annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
|
|
37469
38023
|
allCandidates.sort((a, b) => {
|
|
37470
38024
|
const priorityOrder = {
|
|
37471
38025
|
merged_pr: 0,
|
|
@@ -37479,10 +38033,13 @@ var IssueDiscovery = class {
|
|
|
37479
38033
|
const recDiff = recommendationOrder[a.recommendation] - recommendationOrder[b.recommendation];
|
|
37480
38034
|
if (recDiff !== 0)
|
|
37481
38035
|
return recDiff;
|
|
38036
|
+
const boostDiff = (b.boostScore ?? 0) - (a.boostScore ?? 0);
|
|
38037
|
+
if (boostDiff !== 0)
|
|
38038
|
+
return boostDiff;
|
|
37482
38039
|
return b.viabilityScore - a.viabilityScore;
|
|
37483
38040
|
});
|
|
37484
38041
|
const capped = applyPerRepoCap(allCandidates, 2);
|
|
37485
|
-
info(
|
|
38042
|
+
info(MODULE10, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${capped.length} candidates returned`);
|
|
37486
38043
|
return { candidates: capped.slice(0, maxResults), strategiesUsed };
|
|
37487
38044
|
}
|
|
37488
38045
|
/**
|
|
@@ -37514,15 +38071,375 @@ var IssueDiscovery = class {
|
|
|
37514
38071
|
}
|
|
37515
38072
|
};
|
|
37516
38073
|
|
|
38074
|
+
// ../core/dist/core/roadmap.js
|
|
38075
|
+
var MODULE11 = "roadmap";
|
|
38076
|
+
var CACHE_TTL_MS2 = 60 * 60 * 1e3;
|
|
38077
|
+
var ROADMAP_PATHS = [
|
|
38078
|
+
"ROADMAP.md",
|
|
38079
|
+
"docs/ROADMAP.md",
|
|
38080
|
+
".github/ROADMAP.md",
|
|
38081
|
+
"Roadmap.md",
|
|
38082
|
+
"roadmap.md"
|
|
38083
|
+
];
|
|
38084
|
+
var roadmapCache = /* @__PURE__ */ new Map();
|
|
38085
|
+
function pruneCache2() {
|
|
38086
|
+
const cutoff = Date.now() - CACHE_TTL_MS2;
|
|
38087
|
+
for (const [key, value] of roadmapCache.entries()) {
|
|
38088
|
+
if (value.fetchedAt < cutoff)
|
|
38089
|
+
roadmapCache.delete(key);
|
|
38090
|
+
}
|
|
38091
|
+
}
|
|
38092
|
+
function parseRoadmapIssueRefs(content, owner, repo) {
|
|
38093
|
+
const refs = /* @__PURE__ */ new Set();
|
|
38094
|
+
const ownerRepoLower = `${owner}/${repo}`.toLowerCase();
|
|
38095
|
+
const escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38096
|
+
const fullRefPattern = new RegExp(`\\b${escape2(owner)}/${escape2(repo)}#(\\d+)\\b`, "gi");
|
|
38097
|
+
for (const m of content.matchAll(fullRefPattern)) {
|
|
38098
|
+
const n = Number.parseInt(m[1], 10);
|
|
38099
|
+
if (n > 0)
|
|
38100
|
+
refs.add(n);
|
|
38101
|
+
}
|
|
38102
|
+
const fullRefStripPattern = /\b[\w.-]+\/[\w.-]+#\d+\b/gi;
|
|
38103
|
+
for (const line of content.split("\n")) {
|
|
38104
|
+
if (/^\s*#+\s/.test(line))
|
|
38105
|
+
continue;
|
|
38106
|
+
const stripped = line.replace(fullRefStripPattern, "");
|
|
38107
|
+
for (const m of stripped.matchAll(/(?:^|[^&\w])#(\d+)\b/g)) {
|
|
38108
|
+
const n = Number.parseInt(m[1], 10);
|
|
38109
|
+
if (n > 0)
|
|
38110
|
+
refs.add(n);
|
|
38111
|
+
}
|
|
38112
|
+
}
|
|
38113
|
+
const urlPattern = /https?:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)/gi;
|
|
38114
|
+
for (const m of content.matchAll(urlPattern)) {
|
|
38115
|
+
if (m[1].toLowerCase() === ownerRepoLower) {
|
|
38116
|
+
const n = Number.parseInt(m[2], 10);
|
|
38117
|
+
if (n > 0)
|
|
38118
|
+
refs.add(n);
|
|
38119
|
+
}
|
|
38120
|
+
}
|
|
38121
|
+
return refs;
|
|
38122
|
+
}
|
|
38123
|
+
async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
38124
|
+
const cacheKey = `${owner}/${repo}`.toLowerCase();
|
|
38125
|
+
const cached2 = roadmapCache.get(cacheKey);
|
|
38126
|
+
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS2) {
|
|
38127
|
+
return cached2.refs;
|
|
38128
|
+
}
|
|
38129
|
+
for (const path5 of ROADMAP_PATHS) {
|
|
38130
|
+
try {
|
|
38131
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
|
|
38132
|
+
if (!("content" in data))
|
|
38133
|
+
continue;
|
|
38134
|
+
const content = Buffer.from(data.content, "base64").toString("utf-8");
|
|
38135
|
+
const refs = parseRoadmapIssueRefs(content, owner, repo);
|
|
38136
|
+
roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
|
|
38137
|
+
pruneCache2();
|
|
38138
|
+
return refs;
|
|
38139
|
+
} catch (err) {
|
|
38140
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38141
|
+
throw err;
|
|
38142
|
+
const status = getHttpStatusCode(err);
|
|
38143
|
+
if (status === 404)
|
|
38144
|
+
continue;
|
|
38145
|
+
warn(MODULE11, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(err)}`);
|
|
38146
|
+
}
|
|
38147
|
+
}
|
|
38148
|
+
const empty = /* @__PURE__ */ new Set();
|
|
38149
|
+
roadmapCache.set(cacheKey, { refs: empty, fetchedAt: Date.now() });
|
|
38150
|
+
pruneCache2();
|
|
38151
|
+
return empty;
|
|
38152
|
+
}
|
|
38153
|
+
|
|
38154
|
+
// ../core/dist/core/feature-discovery.js
|
|
38155
|
+
var MODULE12 = "feature-discovery";
|
|
38156
|
+
var INTER_REPO_DELAY_MS = 2e3;
|
|
38157
|
+
var MIN_VIABILITY_SCORE = 40;
|
|
38158
|
+
var ANCHOR_THRESHOLD = 3;
|
|
38159
|
+
var DEFAULT_SPLIT_RATIO = 0.6;
|
|
38160
|
+
function resolveAnchorRepos(repoScores, threshold = ANCHOR_THRESHOLD) {
|
|
38161
|
+
return Object.values(repoScores).filter((rs) => rs.mergedPRCount >= threshold).sort((a, b) => b.mergedPRCount - a.mergedPRCount).map((rs) => rs.repo);
|
|
38162
|
+
}
|
|
38163
|
+
var BIGGER_BET_LABELS = /* @__PURE__ */ new Set([
|
|
38164
|
+
"roadmap",
|
|
38165
|
+
"accepted-rfc",
|
|
38166
|
+
"proposal"
|
|
38167
|
+
]);
|
|
38168
|
+
function classifyHorizon(input) {
|
|
38169
|
+
if (input.hasMilestone || input.isOnRoadmap)
|
|
38170
|
+
return "bigger-bet";
|
|
38171
|
+
for (const label of input.labels) {
|
|
38172
|
+
if (BIGGER_BET_LABELS.has(label.toLowerCase()))
|
|
38173
|
+
return "bigger-bet";
|
|
38174
|
+
}
|
|
38175
|
+
return "quick-win";
|
|
38176
|
+
}
|
|
38177
|
+
function splitByHorizon(candidates, count, ratio = DEFAULT_SPLIT_RATIO) {
|
|
38178
|
+
const allQuick = candidates.filter((c) => c.horizon === "quick-win").sort((a, b) => b.viabilityScore - a.viabilityScore);
|
|
38179
|
+
const allBigger = candidates.filter((c) => c.horizon === "bigger-bet").sort((a, b) => b.viabilityScore - a.viabilityScore);
|
|
38180
|
+
const targetQuick = Math.round(count * ratio);
|
|
38181
|
+
const targetBigger = count - targetQuick;
|
|
38182
|
+
const quickTaken = Math.min(allQuick.length, targetQuick);
|
|
38183
|
+
const biggerTaken = Math.min(allBigger.length, targetBigger);
|
|
38184
|
+
let quickFinal = quickTaken;
|
|
38185
|
+
let biggerFinal = biggerTaken;
|
|
38186
|
+
const quickDeficit = targetQuick - quickTaken;
|
|
38187
|
+
const biggerDeficit = targetBigger - biggerTaken;
|
|
38188
|
+
if (quickDeficit > 0) {
|
|
38189
|
+
biggerFinal = Math.min(allBigger.length, biggerFinal + quickDeficit);
|
|
38190
|
+
}
|
|
38191
|
+
if (biggerDeficit > 0) {
|
|
38192
|
+
quickFinal = Math.min(allQuick.length, quickFinal + biggerDeficit);
|
|
38193
|
+
}
|
|
38194
|
+
return {
|
|
38195
|
+
quickWins: allQuick.slice(0, quickFinal),
|
|
38196
|
+
biggerBets: allBigger.slice(0, biggerFinal)
|
|
38197
|
+
};
|
|
38198
|
+
}
|
|
38199
|
+
var FEATURE_LABELS = [
|
|
38200
|
+
"enhancement",
|
|
38201
|
+
"feature",
|
|
38202
|
+
"feature-request",
|
|
38203
|
+
"proposal",
|
|
38204
|
+
"roadmap",
|
|
38205
|
+
"accepted-rfc"
|
|
38206
|
+
];
|
|
38207
|
+
var FEATURE_EXCLUSION_LABELS = /* @__PURE__ */ new Set([
|
|
38208
|
+
"good first issue",
|
|
38209
|
+
"bug",
|
|
38210
|
+
"documentation"
|
|
38211
|
+
]);
|
|
38212
|
+
var WONTFIX_NO_CONTRIBUTOR_LABELS = /* @__PURE__ */ new Set([
|
|
38213
|
+
"help wanted",
|
|
38214
|
+
"contributions welcome",
|
|
38215
|
+
"up-for-grabs",
|
|
38216
|
+
"bounty",
|
|
38217
|
+
"pinned",
|
|
38218
|
+
"unmaintained"
|
|
38219
|
+
]);
|
|
38220
|
+
var WONTFIX_MIN_AGE_DAYS = 60;
|
|
38221
|
+
function detectWontfixNoContributor(input) {
|
|
38222
|
+
const matched = input.labels.some((l) => WONTFIX_NO_CONTRIBUTOR_LABELS.has(l.toLowerCase()));
|
|
38223
|
+
if (!matched)
|
|
38224
|
+
return false;
|
|
38225
|
+
const created = new Date(input.createdAt);
|
|
38226
|
+
if (Number.isNaN(created.getTime()))
|
|
38227
|
+
return false;
|
|
38228
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
38229
|
+
const ageDays = (now.getTime() - created.getTime()) / (1e3 * 60 * 60 * 24);
|
|
38230
|
+
return ageDays >= (input.minAgeDays ?? WONTFIX_MIN_AGE_DAYS);
|
|
38231
|
+
}
|
|
38232
|
+
var NO_ANCHORS_MESSAGE = "No anchor repos yet (need 3+ merged PRs in a repo). Try `scout search` to build relationships first.";
|
|
38233
|
+
var NO_RESULTS_MESSAGE = "No open feature opportunities in your anchor repos right now. Check back next week, or try `scout search` for fix-mode work.";
|
|
38234
|
+
function extractLabels(item) {
|
|
38235
|
+
if (!Array.isArray(item.labels))
|
|
38236
|
+
return [];
|
|
38237
|
+
return item.labels.map((l) => typeof l === "string" ? l : l?.name).filter((s) => typeof s === "string");
|
|
38238
|
+
}
|
|
38239
|
+
function isFeatureIssue(item) {
|
|
38240
|
+
const labels = extractLabels(item).map((l) => l.toLowerCase());
|
|
38241
|
+
if (labels.length === 0)
|
|
38242
|
+
return false;
|
|
38243
|
+
if (labels.some((l) => FEATURE_EXCLUSION_LABELS.has(l)))
|
|
38244
|
+
return false;
|
|
38245
|
+
return labels.some((l) => FEATURE_LABELS.includes(l));
|
|
38246
|
+
}
|
|
38247
|
+
async function discoverFeatures(opts) {
|
|
38248
|
+
const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold);
|
|
38249
|
+
if (anchorRepos.length === 0) {
|
|
38250
|
+
return {
|
|
38251
|
+
quickWins: [],
|
|
38252
|
+
biggerBets: [],
|
|
38253
|
+
anchorRepos: [],
|
|
38254
|
+
message: NO_ANCHORS_MESSAGE
|
|
38255
|
+
};
|
|
38256
|
+
}
|
|
38257
|
+
const candidates = [];
|
|
38258
|
+
for (let i = 0; i < anchorRepos.length; i++) {
|
|
38259
|
+
if (i > 0)
|
|
38260
|
+
await sleep(INTER_REPO_DELAY_MS);
|
|
38261
|
+
const [owner, repo] = anchorRepos[i].split("/");
|
|
38262
|
+
let response;
|
|
38263
|
+
let roadmapRefs;
|
|
38264
|
+
try {
|
|
38265
|
+
const [listResp, refs] = await Promise.all([
|
|
38266
|
+
opts.octokit.issues.listForRepo({
|
|
38267
|
+
owner,
|
|
38268
|
+
repo,
|
|
38269
|
+
state: "open",
|
|
38270
|
+
sort: "updated",
|
|
38271
|
+
direction: "desc",
|
|
38272
|
+
per_page: 20
|
|
38273
|
+
}),
|
|
38274
|
+
fetchRoadmapIssueRefs(opts.octokit, owner, repo)
|
|
38275
|
+
]);
|
|
38276
|
+
response = listResp;
|
|
38277
|
+
roadmapRefs = refs;
|
|
38278
|
+
} catch (err) {
|
|
38279
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38280
|
+
throw err;
|
|
38281
|
+
warn(MODULE12, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
|
|
38282
|
+
continue;
|
|
38283
|
+
}
|
|
38284
|
+
const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
|
|
38285
|
+
for (const item of items) {
|
|
38286
|
+
const labels = extractLabels(item);
|
|
38287
|
+
const hasMilestone = !!item.milestone;
|
|
38288
|
+
const reactions = item.reactions?.total_count ?? 0;
|
|
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 });
|
|
38315
|
+
}
|
|
38316
|
+
}
|
|
38317
|
+
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
38318
|
+
const split = splitByHorizon(passing, opts.count, opts.splitRatio);
|
|
38319
|
+
const total = split.quickWins.length + split.biggerBets.length;
|
|
38320
|
+
return {
|
|
38321
|
+
...split,
|
|
38322
|
+
anchorRepos,
|
|
38323
|
+
message: total === 0 ? NO_RESULTS_MESSAGE : null
|
|
38324
|
+
};
|
|
38325
|
+
}
|
|
38326
|
+
var NO_BROAD_RESULTS_MESSAGE = "No open feature opportunities matched your filters. Try widening your language preferences in `scout config`.";
|
|
38327
|
+
var DEFAULT_BROAD_MAX_TO_VET = 30;
|
|
38328
|
+
function buildBroadFeatureSearchQuery(opts) {
|
|
38329
|
+
const parts = ["is:issue", "is:open", "no:assignee"];
|
|
38330
|
+
const labelClause = FEATURE_LABELS.map((l) => `label:"${l}"`).join(" OR ");
|
|
38331
|
+
parts.push(`(${labelClause})`);
|
|
38332
|
+
for (const excl of FEATURE_EXCLUSION_LABELS) {
|
|
38333
|
+
parts.push(`-label:"${excl}"`);
|
|
38334
|
+
}
|
|
38335
|
+
const languages = (opts.languages ?? []).filter((l) => l && l.toLowerCase() !== "any");
|
|
38336
|
+
if (languages.length > 0) {
|
|
38337
|
+
const langClause = languages.map((l) => `language:${l}`).join(" OR ");
|
|
38338
|
+
parts.push(`(${langClause})`);
|
|
38339
|
+
}
|
|
38340
|
+
for (const repo of opts.excludeRepos ?? []) {
|
|
38341
|
+
parts.push(`-repo:${repo}`);
|
|
38342
|
+
}
|
|
38343
|
+
for (const org of opts.excludeOrgs ?? []) {
|
|
38344
|
+
parts.push(`-user:${org}`);
|
|
38345
|
+
}
|
|
38346
|
+
return parts.join(" ");
|
|
38347
|
+
}
|
|
38348
|
+
async function discoverFeaturesBroad(opts) {
|
|
38349
|
+
const query = buildBroadFeatureSearchQuery({
|
|
38350
|
+
languages: opts.languages,
|
|
38351
|
+
excludeRepos: opts.excludeRepos,
|
|
38352
|
+
excludeOrgs: opts.excludeOrgs
|
|
38353
|
+
});
|
|
38354
|
+
const maxToVet = opts.maxToVet ?? DEFAULT_BROAD_MAX_TO_VET;
|
|
38355
|
+
let items;
|
|
38356
|
+
try {
|
|
38357
|
+
const response = await opts.octokit.search.issuesAndPullRequests({
|
|
38358
|
+
q: query,
|
|
38359
|
+
sort: "interactions",
|
|
38360
|
+
order: "desc",
|
|
38361
|
+
per_page: maxToVet
|
|
38362
|
+
});
|
|
38363
|
+
items = response.data.items.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
|
|
38364
|
+
} catch (err) {
|
|
38365
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38366
|
+
throw err;
|
|
38367
|
+
warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
|
|
38368
|
+
return {
|
|
38369
|
+
quickWins: [],
|
|
38370
|
+
biggerBets: [],
|
|
38371
|
+
anchorRepos: [],
|
|
38372
|
+
message: NO_BROAD_RESULTS_MESSAGE
|
|
38373
|
+
};
|
|
38374
|
+
}
|
|
38375
|
+
const candidates = [];
|
|
38376
|
+
for (const item of items) {
|
|
38377
|
+
const labels = extractLabels(item);
|
|
38378
|
+
const hasMilestone = !!item.milestone;
|
|
38379
|
+
const reactions = item.reactions?.total_count ?? 0;
|
|
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 });
|
|
38403
|
+
}
|
|
38404
|
+
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
38405
|
+
const split = splitByHorizon(passing, opts.count, opts.splitRatio);
|
|
38406
|
+
const total = split.quickWins.length + split.biggerBets.length;
|
|
38407
|
+
return {
|
|
38408
|
+
...split,
|
|
38409
|
+
anchorRepos: [],
|
|
38410
|
+
message: total === 0 ? NO_BROAD_RESULTS_MESSAGE : null
|
|
38411
|
+
};
|
|
38412
|
+
}
|
|
38413
|
+
|
|
37517
38414
|
// ../core/dist/core/gist-state-store.js
|
|
37518
38415
|
var fs3 = __toESM(require("fs"), 1);
|
|
37519
38416
|
var path3 = __toESM(require("path"), 1);
|
|
37520
|
-
var
|
|
38417
|
+
var MODULE13 = "gist-state";
|
|
37521
38418
|
var GIST_DESCRIPTION = "oss-scout-state";
|
|
37522
38419
|
var GIST_FILENAME = "state.json";
|
|
37523
38420
|
var GIST_ID_FILE = "gist-id";
|
|
37524
38421
|
var CACHE_FILE = "state-cache.json";
|
|
37525
38422
|
var SEARCH_MAX_PAGES = 5;
|
|
38423
|
+
function classifyDegradedReason(err) {
|
|
38424
|
+
if (isRateLimitError(err))
|
|
38425
|
+
return "rate_limit";
|
|
38426
|
+
const status = getHttpStatusCode(err);
|
|
38427
|
+
if (status === 403 && errorMessage(err).toLowerCase().includes("abuse detection")) {
|
|
38428
|
+
return "rate_limit";
|
|
38429
|
+
}
|
|
38430
|
+
if (status !== void 0 && status >= 500 && status < 600)
|
|
38431
|
+
return "server";
|
|
38432
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
38433
|
+
const code = err.code;
|
|
38434
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || code === "ECONNRESET" || code === "EAI_AGAIN") {
|
|
38435
|
+
return "network";
|
|
38436
|
+
}
|
|
38437
|
+
}
|
|
38438
|
+
if (err instanceof Error && err.message.toLowerCase().includes("fetch failed")) {
|
|
38439
|
+
return "network";
|
|
38440
|
+
}
|
|
38441
|
+
return "unknown";
|
|
38442
|
+
}
|
|
37526
38443
|
function getGistIdPath() {
|
|
37527
38444
|
return path3.join(getDataDir(), GIST_ID_FILE);
|
|
37528
38445
|
}
|
|
@@ -37543,8 +38460,10 @@ var GistStateStore = class {
|
|
|
37543
38460
|
try {
|
|
37544
38461
|
return await this.bootstrapFromApi();
|
|
37545
38462
|
} catch (err) {
|
|
37546
|
-
|
|
37547
|
-
|
|
38463
|
+
if (getHttpStatusCode(err) === 401)
|
|
38464
|
+
throw err;
|
|
38465
|
+
warn(MODULE13, `API bootstrap failed: ${errorMessage(err)}`);
|
|
38466
|
+
return this.bootstrapFromCache(classifyDegradedReason(err));
|
|
37548
38467
|
}
|
|
37549
38468
|
}
|
|
37550
38469
|
/**
|
|
@@ -37553,12 +38472,12 @@ var GistStateStore = class {
|
|
|
37553
38472
|
async push(state) {
|
|
37554
38473
|
this.writeCache(state);
|
|
37555
38474
|
if (!this.gistId) {
|
|
37556
|
-
warn(
|
|
38475
|
+
warn(MODULE13, "No gist ID \u2014 cannot push");
|
|
37557
38476
|
return false;
|
|
37558
38477
|
}
|
|
37559
38478
|
const json2 = JSON.stringify(state, null, 2);
|
|
37560
38479
|
if (json2.length > 9e5) {
|
|
37561
|
-
warn(
|
|
38480
|
+
warn(MODULE13, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
37562
38481
|
return false;
|
|
37563
38482
|
}
|
|
37564
38483
|
try {
|
|
@@ -37568,10 +38487,12 @@ var GistStateStore = class {
|
|
|
37568
38487
|
[GIST_FILENAME]: { content: json2 }
|
|
37569
38488
|
}
|
|
37570
38489
|
});
|
|
37571
|
-
debug(
|
|
38490
|
+
debug(MODULE13, "State pushed to gist");
|
|
37572
38491
|
return true;
|
|
37573
38492
|
} catch (err) {
|
|
37574
|
-
|
|
38493
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38494
|
+
throw err;
|
|
38495
|
+
warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
|
|
37575
38496
|
return false;
|
|
37576
38497
|
}
|
|
37577
38498
|
}
|
|
@@ -37588,7 +38509,9 @@ var GistStateStore = class {
|
|
|
37588
38509
|
}
|
|
37589
38510
|
return state;
|
|
37590
38511
|
} catch (err) {
|
|
37591
|
-
|
|
38512
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38513
|
+
throw err;
|
|
38514
|
+
warn(MODULE13, `Failed to pull: ${errorMessage(err)}`);
|
|
37592
38515
|
return null;
|
|
37593
38516
|
}
|
|
37594
38517
|
}
|
|
@@ -37600,7 +38523,7 @@ var GistStateStore = class {
|
|
|
37600
38523
|
async bootstrapFromApi() {
|
|
37601
38524
|
const cachedId = this.readCachedGistId();
|
|
37602
38525
|
if (cachedId) {
|
|
37603
|
-
debug(
|
|
38526
|
+
debug(MODULE13, `Trying cached gist ID: ${cachedId}`);
|
|
37604
38527
|
try {
|
|
37605
38528
|
const state = await this.fetchGistState(cachedId);
|
|
37606
38529
|
if (state) {
|
|
@@ -37609,13 +38532,15 @@ var GistStateStore = class {
|
|
|
37609
38532
|
return { gistId: cachedId, state, created: false };
|
|
37610
38533
|
}
|
|
37611
38534
|
} catch (err) {
|
|
37612
|
-
|
|
38535
|
+
if (getHttpStatusCode(err) !== 404)
|
|
38536
|
+
throw err;
|
|
38537
|
+
debug(MODULE13, `Cached gist gone (404): ${errorMessage(err)}`);
|
|
37613
38538
|
}
|
|
37614
|
-
debug(
|
|
38539
|
+
debug(MODULE13, "Cached gist ID invalid, searching...");
|
|
37615
38540
|
}
|
|
37616
38541
|
const foundId = await this.searchForGist();
|
|
37617
38542
|
if (foundId) {
|
|
37618
|
-
debug(
|
|
38543
|
+
debug(MODULE13, `Found gist via search: ${foundId}`);
|
|
37619
38544
|
this.saveGistId(foundId);
|
|
37620
38545
|
this.gistId = foundId;
|
|
37621
38546
|
const state = await this.fetchGistState(foundId);
|
|
@@ -37623,10 +38548,10 @@ var GistStateStore = class {
|
|
|
37623
38548
|
this.writeCache(state);
|
|
37624
38549
|
return { gistId: foundId, state, created: false };
|
|
37625
38550
|
}
|
|
37626
|
-
warn(
|
|
37627
|
-
return this.bootstrapFromCache();
|
|
38551
|
+
warn(MODULE13, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
|
|
38552
|
+
return this.bootstrapFromCache("unknown");
|
|
37628
38553
|
}
|
|
37629
|
-
debug(
|
|
38554
|
+
debug(MODULE13, "No existing gist found, creating new one");
|
|
37630
38555
|
const freshState = ScoutStateSchema.parse({ version: 1 });
|
|
37631
38556
|
const newId = await this.createGist(freshState);
|
|
37632
38557
|
this.saveGistId(newId);
|
|
@@ -37634,10 +38559,10 @@ var GistStateStore = class {
|
|
|
37634
38559
|
this.writeCache(freshState);
|
|
37635
38560
|
return { gistId: newId, state: freshState, created: true };
|
|
37636
38561
|
}
|
|
37637
|
-
bootstrapFromCache() {
|
|
38562
|
+
bootstrapFromCache(reason) {
|
|
37638
38563
|
const cached2 = this.readCache();
|
|
37639
38564
|
if (cached2) {
|
|
37640
|
-
debug(
|
|
38565
|
+
debug(MODULE13, "Bootstrapped from local cache (degraded mode)");
|
|
37641
38566
|
const cachedId = this.readCachedGistId();
|
|
37642
38567
|
if (cachedId)
|
|
37643
38568
|
this.gistId = cachedId;
|
|
@@ -37645,12 +38570,19 @@ var GistStateStore = class {
|
|
|
37645
38570
|
gistId: cachedId ?? "",
|
|
37646
38571
|
state: cached2,
|
|
37647
38572
|
created: false,
|
|
37648
|
-
degraded: true
|
|
38573
|
+
degraded: true,
|
|
38574
|
+
degradedReason: reason
|
|
37649
38575
|
};
|
|
37650
38576
|
}
|
|
37651
|
-
debug(
|
|
38577
|
+
debug(MODULE13, "No cache available, using fresh state (degraded mode)");
|
|
37652
38578
|
const fresh = ScoutStateSchema.parse({ version: 1 });
|
|
37653
|
-
return {
|
|
38579
|
+
return {
|
|
38580
|
+
gistId: "",
|
|
38581
|
+
state: fresh,
|
|
38582
|
+
created: false,
|
|
38583
|
+
degraded: true,
|
|
38584
|
+
degradedReason: reason
|
|
38585
|
+
};
|
|
37654
38586
|
}
|
|
37655
38587
|
// ── Gist API operations ──────────────────────────────────────────────
|
|
37656
38588
|
async fetchGistState(gistId) {
|
|
@@ -37662,7 +38594,7 @@ var GistStateStore = class {
|
|
|
37662
38594
|
const parsed = JSON.parse(file2.content);
|
|
37663
38595
|
return ScoutStateSchema.parse(parsed);
|
|
37664
38596
|
} catch (err) {
|
|
37665
|
-
warn(
|
|
38597
|
+
warn(MODULE13, `Gist content failed validation: ${errorMessage(err)}`);
|
|
37666
38598
|
return null;
|
|
37667
38599
|
}
|
|
37668
38600
|
}
|
|
@@ -37698,7 +38630,7 @@ var GistStateStore = class {
|
|
|
37698
38630
|
} catch (err) {
|
|
37699
38631
|
const code = err?.code;
|
|
37700
38632
|
if (code !== "ENOENT") {
|
|
37701
|
-
warn(
|
|
38633
|
+
warn(MODULE13, `Failed to read cached gist ID: ${errorMessage(err)}`);
|
|
37702
38634
|
}
|
|
37703
38635
|
return null;
|
|
37704
38636
|
}
|
|
@@ -37713,7 +38645,7 @@ var GistStateStore = class {
|
|
|
37713
38645
|
} catch (err) {
|
|
37714
38646
|
const code = err?.code;
|
|
37715
38647
|
if (code !== "ENOENT") {
|
|
37716
|
-
warn(
|
|
38648
|
+
warn(MODULE13, `Failed to read state cache: ${errorMessage(err)}`);
|
|
37717
38649
|
}
|
|
37718
38650
|
return null;
|
|
37719
38651
|
}
|
|
@@ -37724,7 +38656,7 @@ var GistStateStore = class {
|
|
|
37724
38656
|
mode: 384
|
|
37725
38657
|
});
|
|
37726
38658
|
} catch (err) {
|
|
37727
|
-
warn(
|
|
38659
|
+
warn(MODULE13, `Failed to write cache: ${errorMessage(err)}`);
|
|
37728
38660
|
}
|
|
37729
38661
|
}
|
|
37730
38662
|
};
|
|
@@ -37813,7 +38745,7 @@ function pickFresherTimestamp(a, b) {
|
|
|
37813
38745
|
// ../core/dist/core/local-state.js
|
|
37814
38746
|
var fs4 = __toESM(require("fs"), 1);
|
|
37815
38747
|
var path4 = __toESM(require("path"), 1);
|
|
37816
|
-
var
|
|
38748
|
+
var MODULE14 = "local-state";
|
|
37817
38749
|
function getStatePath() {
|
|
37818
38750
|
return path4.join(getDataDir(), "state.json");
|
|
37819
38751
|
}
|
|
@@ -37827,19 +38759,33 @@ function loadLocalState() {
|
|
|
37827
38759
|
if (code === "ENOENT") {
|
|
37828
38760
|
return ScoutStateSchema.parse({ version: 1 });
|
|
37829
38761
|
}
|
|
37830
|
-
warn(
|
|
38762
|
+
warn(MODULE14, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
|
|
37831
38763
|
try {
|
|
37832
38764
|
const backupPath = `${statePath}.corrupt.${Date.now()}`;
|
|
37833
38765
|
fs4.copyFileSync(statePath, backupPath);
|
|
37834
|
-
warn(
|
|
38766
|
+
warn(MODULE14, `Corrupt state backed up to ${backupPath}`);
|
|
37835
38767
|
} catch (backupErr) {
|
|
37836
|
-
warn(
|
|
38768
|
+
warn(MODULE14, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
37837
38769
|
}
|
|
37838
38770
|
return ScoutStateSchema.parse({ version: 1 });
|
|
37839
38771
|
}
|
|
37840
38772
|
}
|
|
37841
38773
|
|
|
37842
38774
|
// ../core/dist/scout.js
|
|
38775
|
+
function offlineModeMessage(reason) {
|
|
38776
|
+
const tail = "Changes will only be saved locally.";
|
|
38777
|
+
switch (reason) {
|
|
38778
|
+
case "rate_limit":
|
|
38779
|
+
return `Gist sync unavailable \u2014 GitHub API rate limit exceeded. ${tail} Try again after the rate limit resets.`;
|
|
38780
|
+
case "network":
|
|
38781
|
+
return `Gist sync unavailable \u2014 could not reach GitHub. ${tail} Check your network connection.`;
|
|
38782
|
+
case "server":
|
|
38783
|
+
return `Gist sync unavailable \u2014 GitHub returned a server error. ${tail} Try again later.`;
|
|
38784
|
+
case "unknown":
|
|
38785
|
+
case void 0:
|
|
38786
|
+
return `Gist sync unavailable \u2014 running in offline mode. ${tail}`;
|
|
38787
|
+
}
|
|
38788
|
+
}
|
|
37843
38789
|
function toGistOctokit(octokit) {
|
|
37844
38790
|
return {
|
|
37845
38791
|
gists: {
|
|
@@ -37886,7 +38832,7 @@ async function createScout(config2) {
|
|
|
37886
38832
|
gistStore = new GistStateStore(toGistOctokit(getOctokit(config2.githubToken)));
|
|
37887
38833
|
const result = await gistStore.bootstrap();
|
|
37888
38834
|
if (result.degraded) {
|
|
37889
|
-
warn("scout",
|
|
38835
|
+
warn("scout", offlineModeMessage(result.degradedReason));
|
|
37890
38836
|
}
|
|
37891
38837
|
const localState = loadLocalState();
|
|
37892
38838
|
state = mergeStates(localState, result.state);
|
|
@@ -37922,7 +38868,9 @@ var OssScout = class {
|
|
|
37922
38868
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
37923
38869
|
maxResults: options?.maxResults,
|
|
37924
38870
|
strategies: options?.strategies,
|
|
37925
|
-
skippedUrls
|
|
38871
|
+
skippedUrls,
|
|
38872
|
+
preferLanguages: options?.preferLanguages,
|
|
38873
|
+
preferRepos: options?.preferRepos
|
|
37926
38874
|
});
|
|
37927
38875
|
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37928
38876
|
this.dirty = true;
|
|
@@ -37941,6 +38889,45 @@ var OssScout = class {
|
|
|
37941
38889
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
37942
38890
|
return discovery.vetIssue(issueUrl);
|
|
37943
38891
|
}
|
|
38892
|
+
/**
|
|
38893
|
+
* `scout features` — surfaces feature-scoped contribution opportunities
|
|
38894
|
+
* in repos where the user has 3+ merged PRs (configurable via
|
|
38895
|
+
* `featuresAnchorThreshold`), ranked into separate "quick wins" and
|
|
38896
|
+
* "bigger bets" buckets (split via `featuresSplitRatio`).
|
|
38897
|
+
*
|
|
38898
|
+
* Per-call `anchorThreshold` and `splitRatio` overrides take precedence
|
|
38899
|
+
* over the persisted preferences.
|
|
38900
|
+
*
|
|
38901
|
+
* When `broad` is true (#100), bypasses anchor resolution and runs a
|
|
38902
|
+
* cross-repo GitHub Search query for first-touch contributors who
|
|
38903
|
+
* haven't yet built repo relationships. Filters by user language
|
|
38904
|
+
* preferences and excluded repos/orgs.
|
|
38905
|
+
*/
|
|
38906
|
+
async features(options) {
|
|
38907
|
+
const count = options?.count ?? 10;
|
|
38908
|
+
const octokit = getOctokit(this.githubToken);
|
|
38909
|
+
const vetter = new IssueVetter(octokit, this);
|
|
38910
|
+
const result = options?.broad ? await discoverFeaturesBroad({
|
|
38911
|
+
octokit,
|
|
38912
|
+
vetter,
|
|
38913
|
+
count,
|
|
38914
|
+
languages: this.state.preferences.languages,
|
|
38915
|
+
excludeRepos: this.state.preferences.excludeRepos,
|
|
38916
|
+
excludeOrgs: this.state.preferences.excludeOrgs,
|
|
38917
|
+
splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
|
|
38918
|
+
}) : await discoverFeatures({
|
|
38919
|
+
octokit,
|
|
38920
|
+
vetter,
|
|
38921
|
+
repoScores: this.state.repoScores ?? {},
|
|
38922
|
+
count,
|
|
38923
|
+
anchorThreshold: options?.anchorThreshold ?? this.state.preferences.featuresAnchorThreshold,
|
|
38924
|
+
splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
|
|
38925
|
+
});
|
|
38926
|
+
this.saveResults([...result.quickWins, ...result.biggerBets]);
|
|
38927
|
+
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38928
|
+
this.dirty = true;
|
|
38929
|
+
return result;
|
|
38930
|
+
}
|
|
37944
38931
|
// ── Batch Vetting ───────────────────────────────────────────────────
|
|
37945
38932
|
/**
|
|
37946
38933
|
* Re-vet all saved results with bounded concurrency.
|
|
@@ -37952,7 +38939,10 @@ var OssScout = class {
|
|
|
37952
38939
|
const concurrency = options?.concurrency ?? 5;
|
|
37953
38940
|
const results = [];
|
|
37954
38941
|
const pending = /* @__PURE__ */ new Map();
|
|
38942
|
+
let firstHardError = null;
|
|
37955
38943
|
for (const item of saved) {
|
|
38944
|
+
if (firstHardError)
|
|
38945
|
+
break;
|
|
37956
38946
|
const task = this.vetIssue(item.issueUrl).then((candidate) => {
|
|
37957
38947
|
results.push({
|
|
37958
38948
|
issueUrl: item.issueUrl,
|
|
@@ -37964,15 +38954,19 @@ var OssScout = class {
|
|
|
37964
38954
|
viabilityScore: candidate.viabilityScore
|
|
37965
38955
|
});
|
|
37966
38956
|
}).catch((error48) => {
|
|
37967
|
-
|
|
37968
|
-
|
|
38957
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
38958
|
+
firstHardError ??= error48;
|
|
38959
|
+
return;
|
|
38960
|
+
}
|
|
38961
|
+
const status = getHttpStatusCode(error48);
|
|
38962
|
+
const isGone = status === 404 || status === 410;
|
|
37969
38963
|
results.push({
|
|
37970
38964
|
issueUrl: item.issueUrl,
|
|
37971
38965
|
repo: item.repo,
|
|
37972
38966
|
number: item.number,
|
|
37973
38967
|
title: item.title,
|
|
37974
38968
|
status: isGone ? "closed" : "error",
|
|
37975
|
-
errorMessage:
|
|
38969
|
+
errorMessage: errorMessage(error48)
|
|
37976
38970
|
});
|
|
37977
38971
|
}).finally(() => {
|
|
37978
38972
|
pending.delete(item.issueUrl);
|
|
@@ -37983,6 +38977,12 @@ var OssScout = class {
|
|
|
37983
38977
|
}
|
|
37984
38978
|
}
|
|
37985
38979
|
await Promise.allSettled(pending.values());
|
|
38980
|
+
if (firstHardError) {
|
|
38981
|
+
if (results.length > 0) {
|
|
38982
|
+
warn("scout", `vetList aborted mid-batch after ${results.length} result(s) \u2014 discarding partial results due to auth/rate-limit failure`);
|
|
38983
|
+
}
|
|
38984
|
+
throw firstHardError;
|
|
38985
|
+
}
|
|
37986
38986
|
const summary = {
|
|
37987
38987
|
total: results.length,
|
|
37988
38988
|
stillAvailable: results.filter((r) => r.status === "still_available").length,
|
|
@@ -38040,6 +39040,16 @@ var OssScout = class {
|
|
|
38040
39040
|
const score = this.state.repoScores[repo];
|
|
38041
39041
|
return score ? score.score : null;
|
|
38042
39042
|
}
|
|
39043
|
+
/**
|
|
39044
|
+
* Optional SLM pre-triage config read from preferences (oss-autopilot#1122).
|
|
39045
|
+
* Empty `model` disables the call; the vetter treats it as a no-op.
|
|
39046
|
+
*/
|
|
39047
|
+
getSLMTriageConfig() {
|
|
39048
|
+
return {
|
|
39049
|
+
model: this.state.preferences.slmTriageModel ?? "",
|
|
39050
|
+
host: this.state.preferences.slmTriageHost ?? ""
|
|
39051
|
+
};
|
|
39052
|
+
}
|
|
38043
39053
|
/** Get current preferences (read-only). */
|
|
38044
39054
|
getPreferences() {
|
|
38045
39055
|
return this.state.preferences;
|
|
@@ -38158,7 +39168,8 @@ var OssScout = class {
|
|
|
38158
39168
|
searchPriority: c.searchPriority,
|
|
38159
39169
|
firstSeenAt: prev?.firstSeenAt ?? now,
|
|
38160
39170
|
lastSeenAt: now,
|
|
38161
|
-
lastScore: c.viabilityScore
|
|
39171
|
+
lastScore: c.viabilityScore,
|
|
39172
|
+
horizon: "horizon" in c ? c.horizon : void 0
|
|
38162
39173
|
});
|
|
38163
39174
|
}
|
|
38164
39175
|
this.state.savedResults = [...existing.values()];
|
|
@@ -38318,15 +39329,23 @@ function registerTools(server, scout) {
|
|
|
38318
39329
|
maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
|
|
38319
39330
|
strategies: external_exports3.string().optional().describe(
|
|
38320
39331
|
"Comma-separated search strategies: merged, starred, broad, maintained, all"
|
|
39332
|
+
),
|
|
39333
|
+
preferLanguages: external_exports3.array(external_exports3.string()).optional().describe(
|
|
39334
|
+
"Soft-boost ranking for candidates whose repo language matches one of these (case-insensitive). Personalization tier between recommendation and viabilityScore (#1244). Not a filter; only reorders."
|
|
39335
|
+
),
|
|
39336
|
+
preferRepos: external_exports3.array(external_exports3.string()).optional().describe(
|
|
39337
|
+
"Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."
|
|
38321
39338
|
)
|
|
38322
39339
|
},
|
|
38323
|
-
async ({ maxResults, strategies }) => {
|
|
39340
|
+
async ({ maxResults, strategies, preferLanguages, preferRepos }) => {
|
|
38324
39341
|
try {
|
|
38325
39342
|
const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
|
|
38326
39343
|
const result = await withTimeout(
|
|
38327
39344
|
scout.search({
|
|
38328
39345
|
maxResults: maxResults ?? 10,
|
|
38329
|
-
strategies: parsedStrategies
|
|
39346
|
+
strategies: parsedStrategies,
|
|
39347
|
+
preferLanguages,
|
|
39348
|
+
preferRepos
|
|
38330
39349
|
})
|
|
38331
39350
|
);
|
|
38332
39351
|
scout.saveResults(result.candidates);
|
|
@@ -38343,6 +39362,37 @@ function registerTools(server, scout) {
|
|
|
38343
39362
|
}
|
|
38344
39363
|
}
|
|
38345
39364
|
);
|
|
39365
|
+
server.tool(
|
|
39366
|
+
"scout-features",
|
|
39367
|
+
"Surface feature-scoped contribution opportunities in repos where you have 3+ merged PRs",
|
|
39368
|
+
{
|
|
39369
|
+
maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
|
|
39370
|
+
anchorThreshold: external_exports3.number().int().min(1).max(50).optional().describe("Anchor threshold override (default 3)"),
|
|
39371
|
+
splitRatio: external_exports3.number().min(0).max(1).optional().describe("Quick-wins/bigger-bets split ratio (default 0.6)")
|
|
39372
|
+
},
|
|
39373
|
+
async ({ maxResults, anchorThreshold, splitRatio }) => {
|
|
39374
|
+
try {
|
|
39375
|
+
const result = await withTimeout(
|
|
39376
|
+
scout.features({
|
|
39377
|
+
count: maxResults ?? 10,
|
|
39378
|
+
anchorThreshold,
|
|
39379
|
+
splitRatio
|
|
39380
|
+
})
|
|
39381
|
+
);
|
|
39382
|
+
scout.saveResults([...result.quickWins, ...result.biggerBets]);
|
|
39383
|
+
await scout.checkpoint();
|
|
39384
|
+
return {
|
|
39385
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
39386
|
+
};
|
|
39387
|
+
} catch (err) {
|
|
39388
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39389
|
+
return {
|
|
39390
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
39391
|
+
isError: true
|
|
39392
|
+
};
|
|
39393
|
+
}
|
|
39394
|
+
}
|
|
39395
|
+
);
|
|
38346
39396
|
server.tool(
|
|
38347
39397
|
"vet",
|
|
38348
39398
|
"Vet a specific GitHub issue for contribution viability",
|
|
@@ -38476,7 +39526,9 @@ function registerTools(server, scout) {
|
|
|
38476
39526
|
"minRepoScoreThreshold",
|
|
38477
39527
|
"interPhaseDelayMs",
|
|
38478
39528
|
"broadPhaseDelayMs",
|
|
38479
|
-
"skipBroadWhenSufficientResults"
|
|
39529
|
+
"skipBroadWhenSufficientResults",
|
|
39530
|
+
"featuresAnchorThreshold",
|
|
39531
|
+
"featuresSplitRatio"
|
|
38480
39532
|
]);
|
|
38481
39533
|
const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
|
|
38482
39534
|
server.tool(
|