@oss-scout/mcp 0.4.0 → 0.6.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 +1172 -145
- package/dist/tools.js +41 -0
- package/package.json +5 -4
|
@@ -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,
|
|
@@ -35742,6 +35829,11 @@ var StoredClosedPRSchema = external_exports3.object({
|
|
|
35742
35829
|
title: external_exports3.string(),
|
|
35743
35830
|
closedAt: external_exports3.string()
|
|
35744
35831
|
});
|
|
35832
|
+
var StoredOpenPRSchema = external_exports3.object({
|
|
35833
|
+
url: external_exports3.string(),
|
|
35834
|
+
title: external_exports3.string(),
|
|
35835
|
+
openedAt: external_exports3.string()
|
|
35836
|
+
});
|
|
35745
35837
|
var ContributionGuidelinesSchema = external_exports3.object({
|
|
35746
35838
|
branchNamingConvention: external_exports3.string().optional(),
|
|
35747
35839
|
commitMessageFormat: external_exports3.string().optional(),
|
|
@@ -35758,6 +35850,19 @@ var ContributionGuidelinesSchema = external_exports3.object({
|
|
|
35758
35850
|
claRequired: external_exports3.boolean().optional(),
|
|
35759
35851
|
rawContent: external_exports3.string().optional()
|
|
35760
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
|
+
});
|
|
35761
35866
|
var IssueVettingResultSchema = external_exports3.object({
|
|
35762
35867
|
passedAllChecks: external_exports3.boolean(),
|
|
35763
35868
|
checks: external_exports3.object({
|
|
@@ -35768,6 +35873,7 @@ var IssueVettingResultSchema = external_exports3.object({
|
|
|
35768
35873
|
contributionGuidelinesFound: external_exports3.boolean()
|
|
35769
35874
|
}),
|
|
35770
35875
|
contributionGuidelines: ContributionGuidelinesSchema.optional(),
|
|
35876
|
+
linkedPR: LinkedPRSchema.nullable().optional(),
|
|
35771
35877
|
notes: external_exports3.array(external_exports3.string())
|
|
35772
35878
|
});
|
|
35773
35879
|
var TrackedIssueSchema = external_exports3.object({
|
|
@@ -35790,6 +35896,7 @@ var SkippedIssueSchema = external_exports3.object({
|
|
|
35790
35896
|
title: external_exports3.string(),
|
|
35791
35897
|
skippedAt: external_exports3.string()
|
|
35792
35898
|
});
|
|
35899
|
+
var HorizonSchema = external_exports3.enum(["quick-win", "bigger-bet"]);
|
|
35793
35900
|
var SavedCandidateSchema = external_exports3.object({
|
|
35794
35901
|
issueUrl: external_exports3.string(),
|
|
35795
35902
|
repo: external_exports3.string(),
|
|
@@ -35801,7 +35908,8 @@ var SavedCandidateSchema = external_exports3.object({
|
|
|
35801
35908
|
searchPriority: external_exports3.string(),
|
|
35802
35909
|
firstSeenAt: external_exports3.string(),
|
|
35803
35910
|
lastSeenAt: external_exports3.string(),
|
|
35804
|
-
lastScore: external_exports3.number()
|
|
35911
|
+
lastScore: external_exports3.number(),
|
|
35912
|
+
horizon: HorizonSchema.optional()
|
|
35805
35913
|
});
|
|
35806
35914
|
var PersistenceModeSchema = external_exports3.enum(["local", "gist"]);
|
|
35807
35915
|
var ScoutPreferencesSchema = external_exports3.object({
|
|
@@ -35821,7 +35929,32 @@ var ScoutPreferencesSchema = external_exports3.object({
|
|
|
35821
35929
|
persistence: PersistenceModeSchema.default("local"),
|
|
35822
35930
|
defaultStrategy: external_exports3.array(SearchStrategySchema).optional(),
|
|
35823
35931
|
broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
|
|
35824
|
-
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)
|
|
35825
35958
|
});
|
|
35826
35959
|
var ScoutStateSchema = external_exports3.object({
|
|
35827
35960
|
version: external_exports3.literal(1),
|
|
@@ -35831,6 +35964,7 @@ var ScoutStateSchema = external_exports3.object({
|
|
|
35831
35964
|
starredReposLastFetched: external_exports3.string().optional(),
|
|
35832
35965
|
mergedPRs: external_exports3.array(StoredMergedPRSchema).default([]),
|
|
35833
35966
|
closedPRs: external_exports3.array(StoredClosedPRSchema).default([]),
|
|
35967
|
+
openPRs: external_exports3.array(StoredOpenPRSchema).default([]),
|
|
35834
35968
|
savedResults: external_exports3.array(SavedCandidateSchema).default([]),
|
|
35835
35969
|
skippedIssues: external_exports3.array(SkippedIssueSchema).default([]),
|
|
35836
35970
|
lastSearchAt: external_exports3.string().optional(),
|
|
@@ -35966,6 +36100,16 @@ function calculateViabilityScore(params) {
|
|
|
35966
36100
|
if (params.closedWithoutMergeCount > 0 && params.mergedPRCount === 0) {
|
|
35967
36101
|
score -= 15;
|
|
35968
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
|
+
}
|
|
35969
36113
|
return Math.max(0, Math.min(100, score));
|
|
35970
36114
|
}
|
|
35971
36115
|
|
|
@@ -36357,6 +36501,35 @@ function isNotModifiedError(err) {
|
|
|
36357
36501
|
}
|
|
36358
36502
|
|
|
36359
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
|
+
}
|
|
36360
36533
|
var MODULE5 = "issue-eligibility";
|
|
36361
36534
|
var CLAIM_PHRASES = [
|
|
36362
36535
|
"i'm working on this",
|
|
@@ -36384,15 +36557,27 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
|
36384
36557
|
per_page: 100,
|
|
36385
36558
|
page
|
|
36386
36559
|
}));
|
|
36387
|
-
|
|
36560
|
+
let linkedPRCount = 0;
|
|
36561
|
+
let linkedPR = null;
|
|
36562
|
+
for (const event of timeline) {
|
|
36388
36563
|
const e = event;
|
|
36389
|
-
|
|
36390
|
-
|
|
36391
|
-
|
|
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 };
|
|
36392
36574
|
} catch (error48) {
|
|
36575
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36576
|
+
throw error48;
|
|
36577
|
+
}
|
|
36393
36578
|
const errMsg = errorMessage(error48);
|
|
36394
36579
|
warn(MODULE5, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
36395
|
-
return { passed: true, inconclusive: true, reason: errMsg };
|
|
36580
|
+
return { passed: true, inconclusive: true, reason: errMsg, linkedPR: null };
|
|
36396
36581
|
}
|
|
36397
36582
|
}
|
|
36398
36583
|
var MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
@@ -36418,6 +36603,9 @@ async function checkUserMergedPRsInRepo(octokit, owner, repo) {
|
|
|
36418
36603
|
tracker.recordCall();
|
|
36419
36604
|
}
|
|
36420
36605
|
} catch (error48) {
|
|
36606
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36607
|
+
throw error48;
|
|
36608
|
+
}
|
|
36421
36609
|
const errMsg = errorMessage(error48);
|
|
36422
36610
|
warn(MODULE5, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
36423
36611
|
return 0;
|
|
@@ -36442,6 +36630,9 @@ async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount)
|
|
|
36442
36630
|
}
|
|
36443
36631
|
return { passed: true };
|
|
36444
36632
|
} catch (error48) {
|
|
36633
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36634
|
+
throw error48;
|
|
36635
|
+
}
|
|
36445
36636
|
const errMsg = errorMessage(error48);
|
|
36446
36637
|
warn(MODULE5, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
36447
36638
|
return { passed: true, inconclusive: true, reason: errMsg };
|
|
@@ -36514,6 +36705,9 @@ async function checkProjectHealth(octokit, owner, repo) {
|
|
|
36514
36705
|
};
|
|
36515
36706
|
});
|
|
36516
36707
|
} catch (error48) {
|
|
36708
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
36709
|
+
throw error48;
|
|
36710
|
+
}
|
|
36517
36711
|
const errMsg = errorMessage(error48);
|
|
36518
36712
|
warn(MODULE6, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
36519
36713
|
return {
|
|
@@ -36547,6 +36741,13 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
36547
36741
|
}
|
|
36548
36742
|
return null;
|
|
36549
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
|
+
}
|
|
36550
36751
|
for (let i = 0; i < results.length; i++) {
|
|
36551
36752
|
const result = results[i];
|
|
36552
36753
|
if (result.status === "fulfilled" && result.value) {
|
|
@@ -36556,9 +36757,9 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
36556
36757
|
return guidelines;
|
|
36557
36758
|
}
|
|
36558
36759
|
if (result.status === "rejected") {
|
|
36559
|
-
const
|
|
36560
|
-
if (
|
|
36561
|
-
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)}`);
|
|
36562
36763
|
}
|
|
36563
36764
|
}
|
|
36564
36765
|
}
|
|
@@ -36608,8 +36809,274 @@ function parseContributionGuidelines(content) {
|
|
|
36608
36809
|
return guidelines;
|
|
36609
36810
|
}
|
|
36610
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
|
+
|
|
36611
37078
|
// ../core/dist/core/issue-vetting.js
|
|
36612
|
-
var
|
|
37079
|
+
var MODULE8 = "issue-vetting";
|
|
36613
37080
|
var MAX_CONCURRENT_VETTING = 3;
|
|
36614
37081
|
var VETTING_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
36615
37082
|
var IssueVetter = class {
|
|
@@ -36622,13 +37089,19 @@ var IssueVetter = class {
|
|
|
36622
37089
|
/**
|
|
36623
37090
|
* Vet a specific issue — runs all checks and computes recommendation + viability score.
|
|
36624
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.
|
|
36625
37097
|
*/
|
|
36626
|
-
async vetIssue(issueUrl) {
|
|
37098
|
+
async vetIssue(issueUrl, opts) {
|
|
36627
37099
|
const cache = getHttpCache();
|
|
36628
|
-
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}`;
|
|
36629
37102
|
const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
|
|
36630
37103
|
if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
|
|
36631
|
-
debug(
|
|
37104
|
+
debug(MODULE8, `Vetting cache hit for ${issueUrl}`);
|
|
36632
37105
|
return cached2;
|
|
36633
37106
|
}
|
|
36634
37107
|
const parsed = parseGitHubUrl(issueUrl);
|
|
@@ -36651,6 +37124,7 @@ var IssueVetter = class {
|
|
|
36651
37124
|
fetchContributionGuidelines(this.octokit, owner, repo),
|
|
36652
37125
|
hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo)
|
|
36653
37126
|
]);
|
|
37127
|
+
const antiLLMPolicy = await fetchAndScanAntiLLMPolicy(this.octokit, owner, repo, { contributingText: contributionGuidelines?.rawContent });
|
|
36654
37128
|
const noExistingPR = existingPRCheck.passed;
|
|
36655
37129
|
const notClaimed = claimCheck.passed;
|
|
36656
37130
|
const clearRequirements = analyzeRequirements(ghIssue.body || "");
|
|
@@ -36665,6 +37139,7 @@ var IssueVetter = class {
|
|
|
36665
37139
|
contributionGuidelinesFound: !!contributionGuidelines
|
|
36666
37140
|
},
|
|
36667
37141
|
contributionGuidelines,
|
|
37142
|
+
linkedPR: existingPRCheck.linkedPR,
|
|
36668
37143
|
notes: []
|
|
36669
37144
|
};
|
|
36670
37145
|
if (!noExistingPR)
|
|
@@ -36765,7 +37240,8 @@ var IssueVetter = class {
|
|
|
36765
37240
|
mergedPRCount: effectiveMergedCount,
|
|
36766
37241
|
orgHasMergedPRs,
|
|
36767
37242
|
repoQualityBonus,
|
|
36768
|
-
matchesPreferredCategory: matchesCategory
|
|
37243
|
+
matchesPreferredCategory: matchesCategory,
|
|
37244
|
+
featureSignals: opts?.featureSignals
|
|
36769
37245
|
});
|
|
36770
37246
|
const starredRepos = this.stateReader.getStarredRepos();
|
|
36771
37247
|
let searchPriority = "normal";
|
|
@@ -36774,10 +37250,26 @@ var IssueVetter = class {
|
|
|
36774
37250
|
} else if (starredRepos.includes(repoFullName)) {
|
|
36775
37251
|
searchPriority = "starred";
|
|
36776
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
|
+
}
|
|
36777
37267
|
const result = {
|
|
36778
37268
|
issue: trackedIssue,
|
|
36779
37269
|
vettingResult,
|
|
36780
37270
|
projectHealth,
|
|
37271
|
+
antiLLMPolicy,
|
|
37272
|
+
slmTriage,
|
|
36781
37273
|
recommendation,
|
|
36782
37274
|
reasonsToSkip,
|
|
36783
37275
|
reasonsToApprove,
|
|
@@ -36796,9 +37288,12 @@ var IssueVetter = class {
|
|
|
36796
37288
|
let failedVettingCount = 0;
|
|
36797
37289
|
let rateLimitFailures = 0;
|
|
36798
37290
|
let attemptedCount = 0;
|
|
37291
|
+
let firstAuthError = null;
|
|
36799
37292
|
for (const url2 of urls) {
|
|
36800
37293
|
if (candidates.length >= maxResults)
|
|
36801
37294
|
break;
|
|
37295
|
+
if (firstAuthError)
|
|
37296
|
+
break;
|
|
36802
37297
|
attemptedCount++;
|
|
36803
37298
|
const task = this.vetIssue(url2).then((candidate) => {
|
|
36804
37299
|
if (candidates.length < maxResults) {
|
|
@@ -36808,11 +37303,15 @@ var IssueVetter = class {
|
|
|
36808
37303
|
candidates.push(candidate);
|
|
36809
37304
|
}
|
|
36810
37305
|
}).catch((error48) => {
|
|
37306
|
+
if (getHttpStatusCode(error48) === 401) {
|
|
37307
|
+
firstAuthError ??= error48;
|
|
37308
|
+
return;
|
|
37309
|
+
}
|
|
36811
37310
|
failedVettingCount++;
|
|
36812
37311
|
if (isRateLimitError(error48)) {
|
|
36813
37312
|
rateLimitFailures++;
|
|
36814
37313
|
}
|
|
36815
|
-
warn(
|
|
37314
|
+
warn(MODULE8, `Error vetting issue ${url2}:`, errorMessage(error48));
|
|
36816
37315
|
}).finally(() => pending.delete(url2));
|
|
36817
37316
|
pending.set(url2, task);
|
|
36818
37317
|
if (pending.size >= MAX_CONCURRENT_VETTING) {
|
|
@@ -36820,9 +37319,15 @@ var IssueVetter = class {
|
|
|
36820
37319
|
}
|
|
36821
37320
|
}
|
|
36822
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
|
+
}
|
|
36823
37328
|
const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
|
|
36824
37329
|
if (allFailed) {
|
|
36825
|
-
warn(
|
|
37330
|
+
warn(MODULE8, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
36826
37331
|
}
|
|
36827
37332
|
return {
|
|
36828
37333
|
candidates: candidates.slice(0, maxResults),
|
|
@@ -36833,14 +37338,14 @@ var IssueVetter = class {
|
|
|
36833
37338
|
};
|
|
36834
37339
|
|
|
36835
37340
|
// ../core/dist/core/search-phases.js
|
|
36836
|
-
var
|
|
37341
|
+
var MODULE9 = "search-phases";
|
|
36837
37342
|
var GITHUB_MAX_BOOLEAN_OPS = 5;
|
|
36838
37343
|
var INTER_QUERY_DELAY_MS = 2e3;
|
|
36839
37344
|
function chunkLabels(labels, reservedOps = 0) {
|
|
36840
37345
|
const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
|
|
36841
37346
|
if (maxPerChunk < 1) {
|
|
36842
37347
|
if (labels.length > 0) {
|
|
36843
|
-
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.`);
|
|
36844
37349
|
}
|
|
36845
37350
|
return [[]];
|
|
36846
37351
|
}
|
|
@@ -36850,7 +37355,7 @@ function chunkLabels(labels, reservedOps = 0) {
|
|
|
36850
37355
|
for (let i = 0; i < labels.length; i += maxPerChunk) {
|
|
36851
37356
|
chunks.push(labels.slice(i, i + maxPerChunk));
|
|
36852
37357
|
}
|
|
36853
|
-
debug(
|
|
37358
|
+
debug(MODULE9, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
|
|
36854
37359
|
return chunks;
|
|
36855
37360
|
}
|
|
36856
37361
|
function buildLabelQuery(labels) {
|
|
@@ -36887,7 +37392,7 @@ async function cachedSearchIssues(octokit, params) {
|
|
|
36887
37392
|
const cache = getHttpCache();
|
|
36888
37393
|
const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
|
|
36889
37394
|
if (cached2) {
|
|
36890
|
-
debug(
|
|
37395
|
+
debug(MODULE9, `Search cache hit for query`);
|
|
36891
37396
|
return cached2;
|
|
36892
37397
|
}
|
|
36893
37398
|
const tracker = getSearchBudgetTracker();
|
|
@@ -36902,7 +37407,7 @@ async function cachedSearchIssues(octokit, params) {
|
|
|
36902
37407
|
if (data.items.length > 0) {
|
|
36903
37408
|
cache.set(cacheKey, "", data);
|
|
36904
37409
|
} else {
|
|
36905
|
-
debug(
|
|
37410
|
+
debug(MODULE9, `Skipping cache for empty search result (possible rate limit artifact)`);
|
|
36906
37411
|
}
|
|
36907
37412
|
return data;
|
|
36908
37413
|
}
|
|
@@ -36950,10 +37455,10 @@ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResul
|
|
|
36950
37455
|
if (getHttpStatusCode(error48) === 401)
|
|
36951
37456
|
throw error48;
|
|
36952
37457
|
if (isRateLimitError(error48)) {
|
|
36953
|
-
warn(
|
|
37458
|
+
warn(MODULE9, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
36954
37459
|
break;
|
|
36955
37460
|
}
|
|
36956
|
-
warn(
|
|
37461
|
+
warn(MODULE9, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
36957
37462
|
}
|
|
36958
37463
|
}
|
|
36959
37464
|
return items;
|
|
@@ -37004,13 +37509,13 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
|
|
|
37004
37509
|
if (isRateLimitError(error48)) {
|
|
37005
37510
|
rateLimitFailures++;
|
|
37006
37511
|
}
|
|
37007
|
-
warn(
|
|
37512
|
+
warn(MODULE9, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37008
37513
|
}
|
|
37009
37514
|
}
|
|
37010
37515
|
const allReposFailed = failedRepos === repos.length && repos.length > 0;
|
|
37011
37516
|
const rateLimitHit = rateLimitFailures > 0;
|
|
37012
37517
|
if (allReposFailed) {
|
|
37013
|
-
warn(
|
|
37518
|
+
warn(MODULE9, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
37014
37519
|
}
|
|
37015
37520
|
return { candidates, allReposFailed, rateLimitHit };
|
|
37016
37521
|
}
|
|
@@ -37041,7 +37546,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37041
37546
|
const spamRepos = detectLabelFarmingRepos(items);
|
|
37042
37547
|
if (spamRepos.size > 0) {
|
|
37043
37548
|
const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
|
|
37044
|
-
debug(
|
|
37549
|
+
debug(MODULE9, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
|
|
37045
37550
|
}
|
|
37046
37551
|
const itemsToVet = filterIssues(items).filter((item) => {
|
|
37047
37552
|
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
@@ -37052,7 +37557,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37052
37557
|
return excludedRepoSets.every((s) => !s.has(repoFullName));
|
|
37053
37558
|
}).slice(0, remainingNeeded * 2);
|
|
37054
37559
|
if (itemsToVet.length === 0) {
|
|
37055
|
-
debug(
|
|
37560
|
+
debug(MODULE9, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
|
|
37056
37561
|
return { candidates: [], allVetFailed: false, rateLimitHit: false };
|
|
37057
37562
|
}
|
|
37058
37563
|
const { candidates: results, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
|
|
@@ -37064,13 +37569,13 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37064
37569
|
});
|
|
37065
37570
|
const starFilteredCount = results.length - starFiltered.length;
|
|
37066
37571
|
if (starFilteredCount > 0) {
|
|
37067
|
-
debug(
|
|
37572
|
+
debug(MODULE9, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
|
|
37068
37573
|
}
|
|
37069
37574
|
return { candidates: starFiltered, allVetFailed, rateLimitHit };
|
|
37070
37575
|
}
|
|
37071
37576
|
|
|
37072
37577
|
// ../core/dist/core/issue-discovery.js
|
|
37073
|
-
var
|
|
37578
|
+
var MODULE10 = "issue-discovery";
|
|
37074
37579
|
var LOW_BUDGET_THRESHOLD = 20;
|
|
37075
37580
|
var CRITICAL_BUDGET_THRESHOLD = 10;
|
|
37076
37581
|
function buildIssueFilter(config2) {
|
|
@@ -37103,9 +37608,9 @@ function buildIssueFilter(config2) {
|
|
|
37103
37608
|
};
|
|
37104
37609
|
}
|
|
37105
37610
|
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
37106
|
-
info(
|
|
37611
|
+
info(MODULE10, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
37107
37612
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
37108
|
-
info(
|
|
37613
|
+
info(MODULE10, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
37109
37614
|
return {
|
|
37110
37615
|
candidates,
|
|
37111
37616
|
error: allReposFailed ? "All merged-PR repo fetches failed" : null,
|
|
@@ -37113,11 +37618,11 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
|
37113
37618
|
};
|
|
37114
37619
|
}
|
|
37115
37620
|
async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
|
|
37116
|
-
info(
|
|
37621
|
+
info(MODULE10, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
37117
37622
|
const phase1Labels = labels.slice(0, 3);
|
|
37118
37623
|
const reposToSearch = repos.slice(0, 10);
|
|
37119
37624
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
|
|
37120
|
-
info(
|
|
37625
|
+
info(MODULE10, `Found ${candidates.length} candidates from starred repos`);
|
|
37121
37626
|
return {
|
|
37122
37627
|
candidates,
|
|
37123
37628
|
error: allReposFailed ? "All starred repo fetches failed" : null,
|
|
@@ -37125,14 +37630,14 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
|
|
|
37125
37630
|
};
|
|
37126
37631
|
}
|
|
37127
37632
|
async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQualifiers, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
37128
|
-
info(
|
|
37633
|
+
info(MODULE10, "Phase 2: General issue search...");
|
|
37129
37634
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37130
37635
|
const tierLabelGroups = [];
|
|
37131
37636
|
if (scopes && scopes.length > 1) {
|
|
37132
37637
|
for (const scope of scopes) {
|
|
37133
37638
|
const scopeLabels = SCOPE_LABELS[scope] ?? [];
|
|
37134
37639
|
if (scopeLabels.length === 0) {
|
|
37135
|
-
warn(
|
|
37640
|
+
warn(MODULE10, `Scope "${scope}" has no labels, skipping tier`);
|
|
37136
37641
|
continue;
|
|
37137
37642
|
}
|
|
37138
37643
|
tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
|
|
@@ -37152,7 +37657,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37152
37657
|
for (const { tier, tierLabels } of tierLabelGroups) {
|
|
37153
37658
|
try {
|
|
37154
37659
|
const allItems = await searchWithChunkedLabels(octokit, tierLabels, 0, (labelQ) => `${baseQualifiers} ${labelQ}`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
|
|
37155
|
-
info(
|
|
37660
|
+
info(MODULE10, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
|
|
37156
37661
|
const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
|
|
37157
37662
|
tierResults.push(tierCandidates);
|
|
37158
37663
|
for (const c of tierCandidates)
|
|
@@ -37163,7 +37668,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37163
37668
|
if (vetRateLimitHit) {
|
|
37164
37669
|
rateLimitHit = true;
|
|
37165
37670
|
}
|
|
37166
|
-
info(
|
|
37671
|
+
info(MODULE10, `Found ${tierCandidates.length} candidates from ${tier} tier`);
|
|
37167
37672
|
} catch (err) {
|
|
37168
37673
|
if (getHttpStatusCode(err) === 401)
|
|
37169
37674
|
throw err;
|
|
@@ -37172,13 +37677,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37172
37677
|
if (isRateLimitError(err)) {
|
|
37173
37678
|
rateLimitHit = true;
|
|
37174
37679
|
}
|
|
37175
|
-
warn(
|
|
37680
|
+
warn(MODULE10, `Error in ${tier} tier search: ${errMsg}`);
|
|
37176
37681
|
tierResults.push([]);
|
|
37177
37682
|
}
|
|
37178
37683
|
}
|
|
37179
37684
|
const interleaved = interleaveArrays(tierResults);
|
|
37180
37685
|
if (interleaved.length === 0 && error48) {
|
|
37181
|
-
warn(
|
|
37686
|
+
warn(MODULE10, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
|
|
37182
37687
|
}
|
|
37183
37688
|
return {
|
|
37184
37689
|
candidates: interleaved.slice(0, maxResults),
|
|
@@ -37187,17 +37692,17 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37187
37692
|
};
|
|
37188
37693
|
}
|
|
37189
37694
|
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
|
|
37190
|
-
info(
|
|
37695
|
+
info(MODULE10, "Phase 3: Searching actively maintained repos...");
|
|
37191
37696
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37192
37697
|
const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
|
|
37193
37698
|
if (eligibleStarred.length > 0) {
|
|
37194
|
-
info(
|
|
37699
|
+
info(MODULE10, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
|
|
37195
37700
|
const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
|
|
37196
37701
|
if (restItems.length > 0) {
|
|
37197
37702
|
try {
|
|
37198
37703
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
|
|
37199
37704
|
if (candidates.length > 0) {
|
|
37200
|
-
info(
|
|
37705
|
+
info(MODULE10, `Found ${candidates.length} candidates from maintained-repo REST search`);
|
|
37201
37706
|
return {
|
|
37202
37707
|
candidates,
|
|
37203
37708
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37207,11 +37712,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37207
37712
|
} catch (error48) {
|
|
37208
37713
|
if (getHttpStatusCode(error48) === 401)
|
|
37209
37714
|
throw error48;
|
|
37210
|
-
warn(
|
|
37715
|
+
warn(MODULE10, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
|
|
37211
37716
|
}
|
|
37212
37717
|
}
|
|
37213
37718
|
}
|
|
37214
|
-
info(
|
|
37719
|
+
info(MODULE10, "Phase 3: Falling back to Search API...");
|
|
37215
37720
|
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
37216
37721
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
37217
37722
|
const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
|
|
@@ -37225,17 +37730,19 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37225
37730
|
order: "desc",
|
|
37226
37731
|
per_page: maxResults * 3
|
|
37227
37732
|
});
|
|
37228
|
-
info(
|
|
37733
|
+
info(MODULE10, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
37229
37734
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
|
|
37230
|
-
info(
|
|
37735
|
+
info(MODULE10, `Found ${candidates.length} candidates from maintained-repo search`);
|
|
37231
37736
|
return {
|
|
37232
37737
|
candidates,
|
|
37233
37738
|
error: allVetFailed ? "all vetting failed" : null,
|
|
37234
37739
|
rateLimitHit: vetRateLimitHit
|
|
37235
37740
|
};
|
|
37236
37741
|
} catch (error48) {
|
|
37742
|
+
if (getHttpStatusCode(error48) === 401)
|
|
37743
|
+
throw error48;
|
|
37237
37744
|
const errMsg = errorMessage(error48);
|
|
37238
|
-
warn(
|
|
37745
|
+
warn(MODULE10, `Error in maintained-repo search: ${errMsg}`);
|
|
37239
37746
|
return {
|
|
37240
37747
|
candidates: [],
|
|
37241
37748
|
error: errMsg,
|
|
@@ -37319,24 +37826,25 @@ var IssueDiscovery = class {
|
|
|
37319
37826
|
if (rateLimit.remaining < 5) {
|
|
37320
37827
|
const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
|
|
37321
37828
|
this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
|
|
37322
|
-
warn(
|
|
37829
|
+
warn(MODULE10, this.rateLimitWarning);
|
|
37323
37830
|
}
|
|
37324
37831
|
if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
|
|
37325
|
-
info(
|
|
37832
|
+
info(MODULE10, `Search budget critical (${searchBudget} remaining) \u2014 running only Phase 0`);
|
|
37326
37833
|
} else if (searchBudget < LOW_BUDGET_THRESHOLD) {
|
|
37327
|
-
info(
|
|
37834
|
+
info(MODULE10, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
|
|
37328
37835
|
}
|
|
37329
37836
|
} catch (error48) {
|
|
37330
37837
|
if (getHttpStatusCode(error48) === 401)
|
|
37331
37838
|
throw error48;
|
|
37332
37839
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
|
|
37333
|
-
warn(
|
|
37840
|
+
warn(MODULE10, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
|
|
37334
37841
|
}
|
|
37335
37842
|
if (searchBudget <= 0) {
|
|
37336
37843
|
this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
37337
37844
|
return { candidates: [], strategiesUsed: [] };
|
|
37338
37845
|
}
|
|
37339
37846
|
const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
|
|
37847
|
+
const openPRRepos = this.stateReader.getReposWithOpenPRs();
|
|
37340
37848
|
const starredRepos = this.getStarredRepos();
|
|
37341
37849
|
const starredRepoSet = new Set(starredRepos);
|
|
37342
37850
|
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
|
|
@@ -37345,7 +37853,7 @@ var IssueDiscovery = class {
|
|
|
37345
37853
|
const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`.replace(/ +/g, " ").trim();
|
|
37346
37854
|
const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
|
|
37347
37855
|
if (aiBlocklisted.size > 0) {
|
|
37348
|
-
debug(
|
|
37856
|
+
debug(MODULE10, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
|
|
37349
37857
|
}
|
|
37350
37858
|
const filterIssues = buildIssueFilter({
|
|
37351
37859
|
excludedRepos: new Set(config2.excludeRepos),
|
|
@@ -37357,7 +37865,16 @@ var IssueDiscovery = class {
|
|
|
37357
37865
|
now: /* @__PURE__ */ new Date(),
|
|
37358
37866
|
includeDocIssues: config2.includeDocIssues ?? true
|
|
37359
37867
|
});
|
|
37360
|
-
const
|
|
37868
|
+
const seenPhase0 = /* @__PURE__ */ new Set();
|
|
37869
|
+
const phase0Repos = [];
|
|
37870
|
+
for (const repo of [...mergedPRRepos, ...openPRRepos]) {
|
|
37871
|
+
if (seenPhase0.has(repo))
|
|
37872
|
+
continue;
|
|
37873
|
+
seenPhase0.add(repo);
|
|
37874
|
+
phase0Repos.push(repo);
|
|
37875
|
+
if (phase0Repos.length >= 10)
|
|
37876
|
+
break;
|
|
37877
|
+
}
|
|
37361
37878
|
const phase0RepoSet = new Set(phase0Repos);
|
|
37362
37879
|
if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
|
|
37363
37880
|
const remaining = maxResults - allCandidates.length;
|
|
@@ -37372,7 +37889,7 @@ var IssueDiscovery = class {
|
|
|
37372
37889
|
}
|
|
37373
37890
|
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
|
|
37374
37891
|
if (interPhaseDelay > 0) {
|
|
37375
|
-
info(
|
|
37892
|
+
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37376
37893
|
await sleep(interPhaseDelay);
|
|
37377
37894
|
}
|
|
37378
37895
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
@@ -37392,17 +37909,17 @@ var IssueDiscovery = class {
|
|
|
37392
37909
|
const skipThreshold = config2.skipBroadWhenSufficientResults ?? 15;
|
|
37393
37910
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
|
|
37394
37911
|
if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
|
|
37395
|
-
info(
|
|
37912
|
+
info(MODULE10, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
|
|
37396
37913
|
} else {
|
|
37397
37914
|
if (interPhaseDelay > 0) {
|
|
37398
|
-
info(
|
|
37915
|
+
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37399
37916
|
await sleep(interPhaseDelay);
|
|
37400
37917
|
}
|
|
37401
37918
|
if (allCandidates.length > 0 && broadDelay > 0) {
|
|
37402
|
-
info(
|
|
37919
|
+
info(MODULE10, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
|
|
37403
37920
|
await sleep(broadDelay);
|
|
37404
37921
|
} else if (allCandidates.length === 0) {
|
|
37405
|
-
info(
|
|
37922
|
+
info(MODULE10, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
37406
37923
|
}
|
|
37407
37924
|
const remaining = maxResults - allCandidates.length;
|
|
37408
37925
|
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
@@ -37415,7 +37932,7 @@ var IssueDiscovery = class {
|
|
|
37415
37932
|
}
|
|
37416
37933
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
|
|
37417
37934
|
if (interPhaseDelay > 0) {
|
|
37418
|
-
info(
|
|
37935
|
+
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37419
37936
|
await sleep(interPhaseDelay);
|
|
37420
37937
|
}
|
|
37421
37938
|
const remaining = maxResults - allCandidates.length;
|
|
@@ -37466,7 +37983,7 @@ var IssueDiscovery = class {
|
|
|
37466
37983
|
return b.viabilityScore - a.viabilityScore;
|
|
37467
37984
|
});
|
|
37468
37985
|
const capped = applyPerRepoCap(allCandidates, 2);
|
|
37469
|
-
info(
|
|
37986
|
+
info(MODULE10, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${capped.length} candidates returned`);
|
|
37470
37987
|
return { candidates: capped.slice(0, maxResults), strategiesUsed };
|
|
37471
37988
|
}
|
|
37472
37989
|
/**
|
|
@@ -37498,15 +38015,375 @@ var IssueDiscovery = class {
|
|
|
37498
38015
|
}
|
|
37499
38016
|
};
|
|
37500
38017
|
|
|
38018
|
+
// ../core/dist/core/roadmap.js
|
|
38019
|
+
var MODULE11 = "roadmap";
|
|
38020
|
+
var CACHE_TTL_MS2 = 60 * 60 * 1e3;
|
|
38021
|
+
var ROADMAP_PATHS = [
|
|
38022
|
+
"ROADMAP.md",
|
|
38023
|
+
"docs/ROADMAP.md",
|
|
38024
|
+
".github/ROADMAP.md",
|
|
38025
|
+
"Roadmap.md",
|
|
38026
|
+
"roadmap.md"
|
|
38027
|
+
];
|
|
38028
|
+
var roadmapCache = /* @__PURE__ */ new Map();
|
|
38029
|
+
function pruneCache2() {
|
|
38030
|
+
const cutoff = Date.now() - CACHE_TTL_MS2;
|
|
38031
|
+
for (const [key, value] of roadmapCache.entries()) {
|
|
38032
|
+
if (value.fetchedAt < cutoff)
|
|
38033
|
+
roadmapCache.delete(key);
|
|
38034
|
+
}
|
|
38035
|
+
}
|
|
38036
|
+
function parseRoadmapIssueRefs(content, owner, repo) {
|
|
38037
|
+
const refs = /* @__PURE__ */ new Set();
|
|
38038
|
+
const ownerRepoLower = `${owner}/${repo}`.toLowerCase();
|
|
38039
|
+
const escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38040
|
+
const fullRefPattern = new RegExp(`\\b${escape2(owner)}/${escape2(repo)}#(\\d+)\\b`, "gi");
|
|
38041
|
+
for (const m of content.matchAll(fullRefPattern)) {
|
|
38042
|
+
const n = Number.parseInt(m[1], 10);
|
|
38043
|
+
if (n > 0)
|
|
38044
|
+
refs.add(n);
|
|
38045
|
+
}
|
|
38046
|
+
const fullRefStripPattern = /\b[\w.-]+\/[\w.-]+#\d+\b/gi;
|
|
38047
|
+
for (const line of content.split("\n")) {
|
|
38048
|
+
if (/^\s*#+\s/.test(line))
|
|
38049
|
+
continue;
|
|
38050
|
+
const stripped = line.replace(fullRefStripPattern, "");
|
|
38051
|
+
for (const m of stripped.matchAll(/(?:^|[^&\w])#(\d+)\b/g)) {
|
|
38052
|
+
const n = Number.parseInt(m[1], 10);
|
|
38053
|
+
if (n > 0)
|
|
38054
|
+
refs.add(n);
|
|
38055
|
+
}
|
|
38056
|
+
}
|
|
38057
|
+
const urlPattern = /https?:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)/gi;
|
|
38058
|
+
for (const m of content.matchAll(urlPattern)) {
|
|
38059
|
+
if (m[1].toLowerCase() === ownerRepoLower) {
|
|
38060
|
+
const n = Number.parseInt(m[2], 10);
|
|
38061
|
+
if (n > 0)
|
|
38062
|
+
refs.add(n);
|
|
38063
|
+
}
|
|
38064
|
+
}
|
|
38065
|
+
return refs;
|
|
38066
|
+
}
|
|
38067
|
+
async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
38068
|
+
const cacheKey = `${owner}/${repo}`.toLowerCase();
|
|
38069
|
+
const cached2 = roadmapCache.get(cacheKey);
|
|
38070
|
+
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS2) {
|
|
38071
|
+
return cached2.refs;
|
|
38072
|
+
}
|
|
38073
|
+
for (const path5 of ROADMAP_PATHS) {
|
|
38074
|
+
try {
|
|
38075
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
|
|
38076
|
+
if (!("content" in data))
|
|
38077
|
+
continue;
|
|
38078
|
+
const content = Buffer.from(data.content, "base64").toString("utf-8");
|
|
38079
|
+
const refs = parseRoadmapIssueRefs(content, owner, repo);
|
|
38080
|
+
roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
|
|
38081
|
+
pruneCache2();
|
|
38082
|
+
return refs;
|
|
38083
|
+
} catch (err) {
|
|
38084
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38085
|
+
throw err;
|
|
38086
|
+
const status = getHttpStatusCode(err);
|
|
38087
|
+
if (status === 404)
|
|
38088
|
+
continue;
|
|
38089
|
+
warn(MODULE11, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(err)}`);
|
|
38090
|
+
}
|
|
38091
|
+
}
|
|
38092
|
+
const empty = /* @__PURE__ */ new Set();
|
|
38093
|
+
roadmapCache.set(cacheKey, { refs: empty, fetchedAt: Date.now() });
|
|
38094
|
+
pruneCache2();
|
|
38095
|
+
return empty;
|
|
38096
|
+
}
|
|
38097
|
+
|
|
38098
|
+
// ../core/dist/core/feature-discovery.js
|
|
38099
|
+
var MODULE12 = "feature-discovery";
|
|
38100
|
+
var INTER_REPO_DELAY_MS = 2e3;
|
|
38101
|
+
var MIN_VIABILITY_SCORE = 40;
|
|
38102
|
+
var ANCHOR_THRESHOLD = 3;
|
|
38103
|
+
var DEFAULT_SPLIT_RATIO = 0.6;
|
|
38104
|
+
function resolveAnchorRepos(repoScores, threshold = ANCHOR_THRESHOLD) {
|
|
38105
|
+
return Object.values(repoScores).filter((rs) => rs.mergedPRCount >= threshold).sort((a, b) => b.mergedPRCount - a.mergedPRCount).map((rs) => rs.repo);
|
|
38106
|
+
}
|
|
38107
|
+
var BIGGER_BET_LABELS = /* @__PURE__ */ new Set([
|
|
38108
|
+
"roadmap",
|
|
38109
|
+
"accepted-rfc",
|
|
38110
|
+
"proposal"
|
|
38111
|
+
]);
|
|
38112
|
+
function classifyHorizon(input) {
|
|
38113
|
+
if (input.hasMilestone || input.isOnRoadmap)
|
|
38114
|
+
return "bigger-bet";
|
|
38115
|
+
for (const label of input.labels) {
|
|
38116
|
+
if (BIGGER_BET_LABELS.has(label.toLowerCase()))
|
|
38117
|
+
return "bigger-bet";
|
|
38118
|
+
}
|
|
38119
|
+
return "quick-win";
|
|
38120
|
+
}
|
|
38121
|
+
function splitByHorizon(candidates, count, ratio = DEFAULT_SPLIT_RATIO) {
|
|
38122
|
+
const allQuick = candidates.filter((c) => c.horizon === "quick-win").sort((a, b) => b.viabilityScore - a.viabilityScore);
|
|
38123
|
+
const allBigger = candidates.filter((c) => c.horizon === "bigger-bet").sort((a, b) => b.viabilityScore - a.viabilityScore);
|
|
38124
|
+
const targetQuick = Math.round(count * ratio);
|
|
38125
|
+
const targetBigger = count - targetQuick;
|
|
38126
|
+
const quickTaken = Math.min(allQuick.length, targetQuick);
|
|
38127
|
+
const biggerTaken = Math.min(allBigger.length, targetBigger);
|
|
38128
|
+
let quickFinal = quickTaken;
|
|
38129
|
+
let biggerFinal = biggerTaken;
|
|
38130
|
+
const quickDeficit = targetQuick - quickTaken;
|
|
38131
|
+
const biggerDeficit = targetBigger - biggerTaken;
|
|
38132
|
+
if (quickDeficit > 0) {
|
|
38133
|
+
biggerFinal = Math.min(allBigger.length, biggerFinal + quickDeficit);
|
|
38134
|
+
}
|
|
38135
|
+
if (biggerDeficit > 0) {
|
|
38136
|
+
quickFinal = Math.min(allQuick.length, quickFinal + biggerDeficit);
|
|
38137
|
+
}
|
|
38138
|
+
return {
|
|
38139
|
+
quickWins: allQuick.slice(0, quickFinal),
|
|
38140
|
+
biggerBets: allBigger.slice(0, biggerFinal)
|
|
38141
|
+
};
|
|
38142
|
+
}
|
|
38143
|
+
var FEATURE_LABELS = [
|
|
38144
|
+
"enhancement",
|
|
38145
|
+
"feature",
|
|
38146
|
+
"feature-request",
|
|
38147
|
+
"proposal",
|
|
38148
|
+
"roadmap",
|
|
38149
|
+
"accepted-rfc"
|
|
38150
|
+
];
|
|
38151
|
+
var FEATURE_EXCLUSION_LABELS = /* @__PURE__ */ new Set([
|
|
38152
|
+
"good first issue",
|
|
38153
|
+
"bug",
|
|
38154
|
+
"documentation"
|
|
38155
|
+
]);
|
|
38156
|
+
var WONTFIX_NO_CONTRIBUTOR_LABELS = /* @__PURE__ */ new Set([
|
|
38157
|
+
"help wanted",
|
|
38158
|
+
"contributions welcome",
|
|
38159
|
+
"up-for-grabs",
|
|
38160
|
+
"bounty",
|
|
38161
|
+
"pinned",
|
|
38162
|
+
"unmaintained"
|
|
38163
|
+
]);
|
|
38164
|
+
var WONTFIX_MIN_AGE_DAYS = 60;
|
|
38165
|
+
function detectWontfixNoContributor(input) {
|
|
38166
|
+
const matched = input.labels.some((l) => WONTFIX_NO_CONTRIBUTOR_LABELS.has(l.toLowerCase()));
|
|
38167
|
+
if (!matched)
|
|
38168
|
+
return false;
|
|
38169
|
+
const created = new Date(input.createdAt);
|
|
38170
|
+
if (Number.isNaN(created.getTime()))
|
|
38171
|
+
return false;
|
|
38172
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
38173
|
+
const ageDays = (now.getTime() - created.getTime()) / (1e3 * 60 * 60 * 24);
|
|
38174
|
+
return ageDays >= (input.minAgeDays ?? WONTFIX_MIN_AGE_DAYS);
|
|
38175
|
+
}
|
|
38176
|
+
var NO_ANCHORS_MESSAGE = "No anchor repos yet (need 3+ merged PRs in a repo). Try `scout search` to build relationships first.";
|
|
38177
|
+
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.";
|
|
38178
|
+
function extractLabels(item) {
|
|
38179
|
+
if (!Array.isArray(item.labels))
|
|
38180
|
+
return [];
|
|
38181
|
+
return item.labels.map((l) => typeof l === "string" ? l : l?.name).filter((s) => typeof s === "string");
|
|
38182
|
+
}
|
|
38183
|
+
function isFeatureIssue(item) {
|
|
38184
|
+
const labels = extractLabels(item).map((l) => l.toLowerCase());
|
|
38185
|
+
if (labels.length === 0)
|
|
38186
|
+
return false;
|
|
38187
|
+
if (labels.some((l) => FEATURE_EXCLUSION_LABELS.has(l)))
|
|
38188
|
+
return false;
|
|
38189
|
+
return labels.some((l) => FEATURE_LABELS.includes(l));
|
|
38190
|
+
}
|
|
38191
|
+
async function discoverFeatures(opts) {
|
|
38192
|
+
const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold);
|
|
38193
|
+
if (anchorRepos.length === 0) {
|
|
38194
|
+
return {
|
|
38195
|
+
quickWins: [],
|
|
38196
|
+
biggerBets: [],
|
|
38197
|
+
anchorRepos: [],
|
|
38198
|
+
message: NO_ANCHORS_MESSAGE
|
|
38199
|
+
};
|
|
38200
|
+
}
|
|
38201
|
+
const candidates = [];
|
|
38202
|
+
for (let i = 0; i < anchorRepos.length; i++) {
|
|
38203
|
+
if (i > 0)
|
|
38204
|
+
await sleep(INTER_REPO_DELAY_MS);
|
|
38205
|
+
const [owner, repo] = anchorRepos[i].split("/");
|
|
38206
|
+
let response;
|
|
38207
|
+
let roadmapRefs;
|
|
38208
|
+
try {
|
|
38209
|
+
const [listResp, refs] = await Promise.all([
|
|
38210
|
+
opts.octokit.issues.listForRepo({
|
|
38211
|
+
owner,
|
|
38212
|
+
repo,
|
|
38213
|
+
state: "open",
|
|
38214
|
+
sort: "updated",
|
|
38215
|
+
direction: "desc",
|
|
38216
|
+
per_page: 20
|
|
38217
|
+
}),
|
|
38218
|
+
fetchRoadmapIssueRefs(opts.octokit, owner, repo)
|
|
38219
|
+
]);
|
|
38220
|
+
response = listResp;
|
|
38221
|
+
roadmapRefs = refs;
|
|
38222
|
+
} catch (err) {
|
|
38223
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38224
|
+
throw err;
|
|
38225
|
+
warn(MODULE12, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
|
|
38226
|
+
continue;
|
|
38227
|
+
}
|
|
38228
|
+
const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
|
|
38229
|
+
for (const item of items) {
|
|
38230
|
+
const labels = extractLabels(item);
|
|
38231
|
+
const hasMilestone = !!item.milestone;
|
|
38232
|
+
const reactions = item.reactions?.total_count ?? 0;
|
|
38233
|
+
const comments = item.comments ?? 0;
|
|
38234
|
+
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38235
|
+
const onRoadmap = typeof item.number === "number" && roadmapRefs.has(item.number);
|
|
38236
|
+
let candidate;
|
|
38237
|
+
try {
|
|
38238
|
+
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38239
|
+
featureSignals: {
|
|
38240
|
+
reactions,
|
|
38241
|
+
comments,
|
|
38242
|
+
hasMilestone,
|
|
38243
|
+
wontfixNoContributor,
|
|
38244
|
+
onRoadmap
|
|
38245
|
+
}
|
|
38246
|
+
});
|
|
38247
|
+
} catch (err) {
|
|
38248
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38249
|
+
throw err;
|
|
38250
|
+
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38251
|
+
continue;
|
|
38252
|
+
}
|
|
38253
|
+
const horizon = classifyHorizon({
|
|
38254
|
+
hasMilestone,
|
|
38255
|
+
labels,
|
|
38256
|
+
isOnRoadmap: onRoadmap
|
|
38257
|
+
});
|
|
38258
|
+
candidates.push({ ...candidate, horizon });
|
|
38259
|
+
}
|
|
38260
|
+
}
|
|
38261
|
+
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
38262
|
+
const split = splitByHorizon(passing, opts.count, opts.splitRatio);
|
|
38263
|
+
const total = split.quickWins.length + split.biggerBets.length;
|
|
38264
|
+
return {
|
|
38265
|
+
...split,
|
|
38266
|
+
anchorRepos,
|
|
38267
|
+
message: total === 0 ? NO_RESULTS_MESSAGE : null
|
|
38268
|
+
};
|
|
38269
|
+
}
|
|
38270
|
+
var NO_BROAD_RESULTS_MESSAGE = "No open feature opportunities matched your filters. Try widening your language preferences in `scout config`.";
|
|
38271
|
+
var DEFAULT_BROAD_MAX_TO_VET = 30;
|
|
38272
|
+
function buildBroadFeatureSearchQuery(opts) {
|
|
38273
|
+
const parts = ["is:issue", "is:open", "no:assignee"];
|
|
38274
|
+
const labelClause = FEATURE_LABELS.map((l) => `label:"${l}"`).join(" OR ");
|
|
38275
|
+
parts.push(`(${labelClause})`);
|
|
38276
|
+
for (const excl of FEATURE_EXCLUSION_LABELS) {
|
|
38277
|
+
parts.push(`-label:"${excl}"`);
|
|
38278
|
+
}
|
|
38279
|
+
const languages = (opts.languages ?? []).filter((l) => l && l.toLowerCase() !== "any");
|
|
38280
|
+
if (languages.length > 0) {
|
|
38281
|
+
const langClause = languages.map((l) => `language:${l}`).join(" OR ");
|
|
38282
|
+
parts.push(`(${langClause})`);
|
|
38283
|
+
}
|
|
38284
|
+
for (const repo of opts.excludeRepos ?? []) {
|
|
38285
|
+
parts.push(`-repo:${repo}`);
|
|
38286
|
+
}
|
|
38287
|
+
for (const org of opts.excludeOrgs ?? []) {
|
|
38288
|
+
parts.push(`-user:${org}`);
|
|
38289
|
+
}
|
|
38290
|
+
return parts.join(" ");
|
|
38291
|
+
}
|
|
38292
|
+
async function discoverFeaturesBroad(opts) {
|
|
38293
|
+
const query = buildBroadFeatureSearchQuery({
|
|
38294
|
+
languages: opts.languages,
|
|
38295
|
+
excludeRepos: opts.excludeRepos,
|
|
38296
|
+
excludeOrgs: opts.excludeOrgs
|
|
38297
|
+
});
|
|
38298
|
+
const maxToVet = opts.maxToVet ?? DEFAULT_BROAD_MAX_TO_VET;
|
|
38299
|
+
let items;
|
|
38300
|
+
try {
|
|
38301
|
+
const response = await opts.octokit.search.issuesAndPullRequests({
|
|
38302
|
+
q: query,
|
|
38303
|
+
sort: "interactions",
|
|
38304
|
+
order: "desc",
|
|
38305
|
+
per_page: maxToVet
|
|
38306
|
+
});
|
|
38307
|
+
items = response.data.items.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
|
|
38308
|
+
} catch (err) {
|
|
38309
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38310
|
+
throw err;
|
|
38311
|
+
warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
|
|
38312
|
+
return {
|
|
38313
|
+
quickWins: [],
|
|
38314
|
+
biggerBets: [],
|
|
38315
|
+
anchorRepos: [],
|
|
38316
|
+
message: NO_BROAD_RESULTS_MESSAGE
|
|
38317
|
+
};
|
|
38318
|
+
}
|
|
38319
|
+
const candidates = [];
|
|
38320
|
+
for (const item of items) {
|
|
38321
|
+
const labels = extractLabels(item);
|
|
38322
|
+
const hasMilestone = !!item.milestone;
|
|
38323
|
+
const reactions = item.reactions?.total_count ?? 0;
|
|
38324
|
+
const comments = item.comments ?? 0;
|
|
38325
|
+
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38326
|
+
let candidate;
|
|
38327
|
+
try {
|
|
38328
|
+
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38329
|
+
featureSignals: {
|
|
38330
|
+
reactions,
|
|
38331
|
+
comments,
|
|
38332
|
+
hasMilestone,
|
|
38333
|
+
wontfixNoContributor
|
|
38334
|
+
// Roadmap scraping is per-repo and would require an extra fetch
|
|
38335
|
+
// per unique repo in the broad result set — deliberately skipped
|
|
38336
|
+
// here to keep the broad path cheap. Anchor mode keeps the bonus.
|
|
38337
|
+
}
|
|
38338
|
+
});
|
|
38339
|
+
} catch (err) {
|
|
38340
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38341
|
+
throw err;
|
|
38342
|
+
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38343
|
+
continue;
|
|
38344
|
+
}
|
|
38345
|
+
const horizon = classifyHorizon({ hasMilestone, labels });
|
|
38346
|
+
candidates.push({ ...candidate, horizon });
|
|
38347
|
+
}
|
|
38348
|
+
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
38349
|
+
const split = splitByHorizon(passing, opts.count, opts.splitRatio);
|
|
38350
|
+
const total = split.quickWins.length + split.biggerBets.length;
|
|
38351
|
+
return {
|
|
38352
|
+
...split,
|
|
38353
|
+
anchorRepos: [],
|
|
38354
|
+
message: total === 0 ? NO_BROAD_RESULTS_MESSAGE : null
|
|
38355
|
+
};
|
|
38356
|
+
}
|
|
38357
|
+
|
|
37501
38358
|
// ../core/dist/core/gist-state-store.js
|
|
37502
38359
|
var fs3 = __toESM(require("fs"), 1);
|
|
37503
38360
|
var path3 = __toESM(require("path"), 1);
|
|
37504
|
-
var
|
|
38361
|
+
var MODULE13 = "gist-state";
|
|
37505
38362
|
var GIST_DESCRIPTION = "oss-scout-state";
|
|
37506
38363
|
var GIST_FILENAME = "state.json";
|
|
37507
38364
|
var GIST_ID_FILE = "gist-id";
|
|
37508
38365
|
var CACHE_FILE = "state-cache.json";
|
|
37509
38366
|
var SEARCH_MAX_PAGES = 5;
|
|
38367
|
+
function classifyDegradedReason(err) {
|
|
38368
|
+
if (isRateLimitError(err))
|
|
38369
|
+
return "rate_limit";
|
|
38370
|
+
const status = getHttpStatusCode(err);
|
|
38371
|
+
if (status === 403 && errorMessage(err).toLowerCase().includes("abuse detection")) {
|
|
38372
|
+
return "rate_limit";
|
|
38373
|
+
}
|
|
38374
|
+
if (status !== void 0 && status >= 500 && status < 600)
|
|
38375
|
+
return "server";
|
|
38376
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
38377
|
+
const code = err.code;
|
|
38378
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || code === "ECONNRESET" || code === "EAI_AGAIN") {
|
|
38379
|
+
return "network";
|
|
38380
|
+
}
|
|
38381
|
+
}
|
|
38382
|
+
if (err instanceof Error && err.message.toLowerCase().includes("fetch failed")) {
|
|
38383
|
+
return "network";
|
|
38384
|
+
}
|
|
38385
|
+
return "unknown";
|
|
38386
|
+
}
|
|
37510
38387
|
function getGistIdPath() {
|
|
37511
38388
|
return path3.join(getDataDir(), GIST_ID_FILE);
|
|
37512
38389
|
}
|
|
@@ -37527,8 +38404,10 @@ var GistStateStore = class {
|
|
|
37527
38404
|
try {
|
|
37528
38405
|
return await this.bootstrapFromApi();
|
|
37529
38406
|
} catch (err) {
|
|
37530
|
-
|
|
37531
|
-
|
|
38407
|
+
if (getHttpStatusCode(err) === 401)
|
|
38408
|
+
throw err;
|
|
38409
|
+
warn(MODULE13, `API bootstrap failed: ${errorMessage(err)}`);
|
|
38410
|
+
return this.bootstrapFromCache(classifyDegradedReason(err));
|
|
37532
38411
|
}
|
|
37533
38412
|
}
|
|
37534
38413
|
/**
|
|
@@ -37537,12 +38416,12 @@ var GistStateStore = class {
|
|
|
37537
38416
|
async push(state) {
|
|
37538
38417
|
this.writeCache(state);
|
|
37539
38418
|
if (!this.gistId) {
|
|
37540
|
-
warn(
|
|
38419
|
+
warn(MODULE13, "No gist ID \u2014 cannot push");
|
|
37541
38420
|
return false;
|
|
37542
38421
|
}
|
|
37543
38422
|
const json2 = JSON.stringify(state, null, 2);
|
|
37544
38423
|
if (json2.length > 9e5) {
|
|
37545
|
-
warn(
|
|
38424
|
+
warn(MODULE13, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
37546
38425
|
return false;
|
|
37547
38426
|
}
|
|
37548
38427
|
try {
|
|
@@ -37552,10 +38431,12 @@ var GistStateStore = class {
|
|
|
37552
38431
|
[GIST_FILENAME]: { content: json2 }
|
|
37553
38432
|
}
|
|
37554
38433
|
});
|
|
37555
|
-
debug(
|
|
38434
|
+
debug(MODULE13, "State pushed to gist");
|
|
37556
38435
|
return true;
|
|
37557
38436
|
} catch (err) {
|
|
37558
|
-
|
|
38437
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38438
|
+
throw err;
|
|
38439
|
+
warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
|
|
37559
38440
|
return false;
|
|
37560
38441
|
}
|
|
37561
38442
|
}
|
|
@@ -37572,7 +38453,9 @@ var GistStateStore = class {
|
|
|
37572
38453
|
}
|
|
37573
38454
|
return state;
|
|
37574
38455
|
} catch (err) {
|
|
37575
|
-
|
|
38456
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38457
|
+
throw err;
|
|
38458
|
+
warn(MODULE13, `Failed to pull: ${errorMessage(err)}`);
|
|
37576
38459
|
return null;
|
|
37577
38460
|
}
|
|
37578
38461
|
}
|
|
@@ -37584,7 +38467,7 @@ var GistStateStore = class {
|
|
|
37584
38467
|
async bootstrapFromApi() {
|
|
37585
38468
|
const cachedId = this.readCachedGistId();
|
|
37586
38469
|
if (cachedId) {
|
|
37587
|
-
debug(
|
|
38470
|
+
debug(MODULE13, `Trying cached gist ID: ${cachedId}`);
|
|
37588
38471
|
try {
|
|
37589
38472
|
const state = await this.fetchGistState(cachedId);
|
|
37590
38473
|
if (state) {
|
|
@@ -37593,13 +38476,15 @@ var GistStateStore = class {
|
|
|
37593
38476
|
return { gistId: cachedId, state, created: false };
|
|
37594
38477
|
}
|
|
37595
38478
|
} catch (err) {
|
|
37596
|
-
|
|
38479
|
+
if (getHttpStatusCode(err) !== 404)
|
|
38480
|
+
throw err;
|
|
38481
|
+
debug(MODULE13, `Cached gist gone (404): ${errorMessage(err)}`);
|
|
37597
38482
|
}
|
|
37598
|
-
debug(
|
|
38483
|
+
debug(MODULE13, "Cached gist ID invalid, searching...");
|
|
37599
38484
|
}
|
|
37600
38485
|
const foundId = await this.searchForGist();
|
|
37601
38486
|
if (foundId) {
|
|
37602
|
-
debug(
|
|
38487
|
+
debug(MODULE13, `Found gist via search: ${foundId}`);
|
|
37603
38488
|
this.saveGistId(foundId);
|
|
37604
38489
|
this.gistId = foundId;
|
|
37605
38490
|
const state = await this.fetchGistState(foundId);
|
|
@@ -37607,10 +38492,10 @@ var GistStateStore = class {
|
|
|
37607
38492
|
this.writeCache(state);
|
|
37608
38493
|
return { gistId: foundId, state, created: false };
|
|
37609
38494
|
}
|
|
37610
|
-
warn(
|
|
37611
|
-
return this.bootstrapFromCache();
|
|
38495
|
+
warn(MODULE13, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
|
|
38496
|
+
return this.bootstrapFromCache("unknown");
|
|
37612
38497
|
}
|
|
37613
|
-
debug(
|
|
38498
|
+
debug(MODULE13, "No existing gist found, creating new one");
|
|
37614
38499
|
const freshState = ScoutStateSchema.parse({ version: 1 });
|
|
37615
38500
|
const newId = await this.createGist(freshState);
|
|
37616
38501
|
this.saveGistId(newId);
|
|
@@ -37618,10 +38503,10 @@ var GistStateStore = class {
|
|
|
37618
38503
|
this.writeCache(freshState);
|
|
37619
38504
|
return { gistId: newId, state: freshState, created: true };
|
|
37620
38505
|
}
|
|
37621
|
-
bootstrapFromCache() {
|
|
38506
|
+
bootstrapFromCache(reason) {
|
|
37622
38507
|
const cached2 = this.readCache();
|
|
37623
38508
|
if (cached2) {
|
|
37624
|
-
debug(
|
|
38509
|
+
debug(MODULE13, "Bootstrapped from local cache (degraded mode)");
|
|
37625
38510
|
const cachedId = this.readCachedGistId();
|
|
37626
38511
|
if (cachedId)
|
|
37627
38512
|
this.gistId = cachedId;
|
|
@@ -37629,12 +38514,19 @@ var GistStateStore = class {
|
|
|
37629
38514
|
gistId: cachedId ?? "",
|
|
37630
38515
|
state: cached2,
|
|
37631
38516
|
created: false,
|
|
37632
|
-
degraded: true
|
|
38517
|
+
degraded: true,
|
|
38518
|
+
degradedReason: reason
|
|
37633
38519
|
};
|
|
37634
38520
|
}
|
|
37635
|
-
debug(
|
|
38521
|
+
debug(MODULE13, "No cache available, using fresh state (degraded mode)");
|
|
37636
38522
|
const fresh = ScoutStateSchema.parse({ version: 1 });
|
|
37637
|
-
return {
|
|
38523
|
+
return {
|
|
38524
|
+
gistId: "",
|
|
38525
|
+
state: fresh,
|
|
38526
|
+
created: false,
|
|
38527
|
+
degraded: true,
|
|
38528
|
+
degradedReason: reason
|
|
38529
|
+
};
|
|
37638
38530
|
}
|
|
37639
38531
|
// ── Gist API operations ──────────────────────────────────────────────
|
|
37640
38532
|
async fetchGistState(gistId) {
|
|
@@ -37646,7 +38538,7 @@ var GistStateStore = class {
|
|
|
37646
38538
|
const parsed = JSON.parse(file2.content);
|
|
37647
38539
|
return ScoutStateSchema.parse(parsed);
|
|
37648
38540
|
} catch (err) {
|
|
37649
|
-
warn(
|
|
38541
|
+
warn(MODULE13, `Gist content failed validation: ${errorMessage(err)}`);
|
|
37650
38542
|
return null;
|
|
37651
38543
|
}
|
|
37652
38544
|
}
|
|
@@ -37682,7 +38574,7 @@ var GistStateStore = class {
|
|
|
37682
38574
|
} catch (err) {
|
|
37683
38575
|
const code = err?.code;
|
|
37684
38576
|
if (code !== "ENOENT") {
|
|
37685
|
-
warn(
|
|
38577
|
+
warn(MODULE13, `Failed to read cached gist ID: ${errorMessage(err)}`);
|
|
37686
38578
|
}
|
|
37687
38579
|
return null;
|
|
37688
38580
|
}
|
|
@@ -37697,7 +38589,7 @@ var GistStateStore = class {
|
|
|
37697
38589
|
} catch (err) {
|
|
37698
38590
|
const code = err?.code;
|
|
37699
38591
|
if (code !== "ENOENT") {
|
|
37700
|
-
warn(
|
|
38592
|
+
warn(MODULE13, `Failed to read state cache: ${errorMessage(err)}`);
|
|
37701
38593
|
}
|
|
37702
38594
|
return null;
|
|
37703
38595
|
}
|
|
@@ -37708,7 +38600,7 @@ var GistStateStore = class {
|
|
|
37708
38600
|
mode: 384
|
|
37709
38601
|
});
|
|
37710
38602
|
} catch (err) {
|
|
37711
|
-
warn(
|
|
38603
|
+
warn(MODULE13, `Failed to write cache: ${errorMessage(err)}`);
|
|
37712
38604
|
}
|
|
37713
38605
|
}
|
|
37714
38606
|
};
|
|
@@ -37721,6 +38613,7 @@ function mergeStates(local, remote) {
|
|
|
37721
38613
|
starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
|
|
37722
38614
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
37723
38615
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
38616
|
+
openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
|
|
37724
38617
|
savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
|
|
37725
38618
|
skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
|
|
37726
38619
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
@@ -37796,7 +38689,7 @@ function pickFresherTimestamp(a, b) {
|
|
|
37796
38689
|
// ../core/dist/core/local-state.js
|
|
37797
38690
|
var fs4 = __toESM(require("fs"), 1);
|
|
37798
38691
|
var path4 = __toESM(require("path"), 1);
|
|
37799
|
-
var
|
|
38692
|
+
var MODULE14 = "local-state";
|
|
37800
38693
|
function getStatePath() {
|
|
37801
38694
|
return path4.join(getDataDir(), "state.json");
|
|
37802
38695
|
}
|
|
@@ -37810,19 +38703,33 @@ function loadLocalState() {
|
|
|
37810
38703
|
if (code === "ENOENT") {
|
|
37811
38704
|
return ScoutStateSchema.parse({ version: 1 });
|
|
37812
38705
|
}
|
|
37813
|
-
warn(
|
|
38706
|
+
warn(MODULE14, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
|
|
37814
38707
|
try {
|
|
37815
38708
|
const backupPath = `${statePath}.corrupt.${Date.now()}`;
|
|
37816
38709
|
fs4.copyFileSync(statePath, backupPath);
|
|
37817
|
-
warn(
|
|
38710
|
+
warn(MODULE14, `Corrupt state backed up to ${backupPath}`);
|
|
37818
38711
|
} catch (backupErr) {
|
|
37819
|
-
warn(
|
|
38712
|
+
warn(MODULE14, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
37820
38713
|
}
|
|
37821
38714
|
return ScoutStateSchema.parse({ version: 1 });
|
|
37822
38715
|
}
|
|
37823
38716
|
}
|
|
37824
38717
|
|
|
37825
38718
|
// ../core/dist/scout.js
|
|
38719
|
+
function offlineModeMessage(reason) {
|
|
38720
|
+
const tail = "Changes will only be saved locally.";
|
|
38721
|
+
switch (reason) {
|
|
38722
|
+
case "rate_limit":
|
|
38723
|
+
return `Gist sync unavailable \u2014 GitHub API rate limit exceeded. ${tail} Try again after the rate limit resets.`;
|
|
38724
|
+
case "network":
|
|
38725
|
+
return `Gist sync unavailable \u2014 could not reach GitHub. ${tail} Check your network connection.`;
|
|
38726
|
+
case "server":
|
|
38727
|
+
return `Gist sync unavailable \u2014 GitHub returned a server error. ${tail} Try again later.`;
|
|
38728
|
+
case "unknown":
|
|
38729
|
+
case void 0:
|
|
38730
|
+
return `Gist sync unavailable \u2014 running in offline mode. ${tail}`;
|
|
38731
|
+
}
|
|
38732
|
+
}
|
|
37826
38733
|
function toGistOctokit(octokit) {
|
|
37827
38734
|
return {
|
|
37828
38735
|
gists: {
|
|
@@ -37869,7 +38776,7 @@ async function createScout(config2) {
|
|
|
37869
38776
|
gistStore = new GistStateStore(toGistOctokit(getOctokit(config2.githubToken)));
|
|
37870
38777
|
const result = await gistStore.bootstrap();
|
|
37871
38778
|
if (result.degraded) {
|
|
37872
|
-
warn("scout",
|
|
38779
|
+
warn("scout", offlineModeMessage(result.degradedReason));
|
|
37873
38780
|
}
|
|
37874
38781
|
const localState = loadLocalState();
|
|
37875
38782
|
state = mergeStates(localState, result.state);
|
|
@@ -37924,6 +38831,45 @@ var OssScout = class {
|
|
|
37924
38831
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
37925
38832
|
return discovery.vetIssue(issueUrl);
|
|
37926
38833
|
}
|
|
38834
|
+
/**
|
|
38835
|
+
* `scout features` — surfaces feature-scoped contribution opportunities
|
|
38836
|
+
* in repos where the user has 3+ merged PRs (configurable via
|
|
38837
|
+
* `featuresAnchorThreshold`), ranked into separate "quick wins" and
|
|
38838
|
+
* "bigger bets" buckets (split via `featuresSplitRatio`).
|
|
38839
|
+
*
|
|
38840
|
+
* Per-call `anchorThreshold` and `splitRatio` overrides take precedence
|
|
38841
|
+
* over the persisted preferences.
|
|
38842
|
+
*
|
|
38843
|
+
* When `broad` is true (#100), bypasses anchor resolution and runs a
|
|
38844
|
+
* cross-repo GitHub Search query for first-touch contributors who
|
|
38845
|
+
* haven't yet built repo relationships. Filters by user language
|
|
38846
|
+
* preferences and excluded repos/orgs.
|
|
38847
|
+
*/
|
|
38848
|
+
async features(options) {
|
|
38849
|
+
const count = options?.count ?? 10;
|
|
38850
|
+
const octokit = getOctokit(this.githubToken);
|
|
38851
|
+
const vetter = new IssueVetter(octokit, this);
|
|
38852
|
+
const result = options?.broad ? await discoverFeaturesBroad({
|
|
38853
|
+
octokit,
|
|
38854
|
+
vetter,
|
|
38855
|
+
count,
|
|
38856
|
+
languages: this.state.preferences.languages,
|
|
38857
|
+
excludeRepos: this.state.preferences.excludeRepos,
|
|
38858
|
+
excludeOrgs: this.state.preferences.excludeOrgs,
|
|
38859
|
+
splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
|
|
38860
|
+
}) : await discoverFeatures({
|
|
38861
|
+
octokit,
|
|
38862
|
+
vetter,
|
|
38863
|
+
repoScores: this.state.repoScores ?? {},
|
|
38864
|
+
count,
|
|
38865
|
+
anchorThreshold: options?.anchorThreshold ?? this.state.preferences.featuresAnchorThreshold,
|
|
38866
|
+
splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
|
|
38867
|
+
});
|
|
38868
|
+
this.saveResults([...result.quickWins, ...result.biggerBets]);
|
|
38869
|
+
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38870
|
+
this.dirty = true;
|
|
38871
|
+
return result;
|
|
38872
|
+
}
|
|
37927
38873
|
// ── Batch Vetting ───────────────────────────────────────────────────
|
|
37928
38874
|
/**
|
|
37929
38875
|
* Re-vet all saved results with bounded concurrency.
|
|
@@ -37935,7 +38881,10 @@ var OssScout = class {
|
|
|
37935
38881
|
const concurrency = options?.concurrency ?? 5;
|
|
37936
38882
|
const results = [];
|
|
37937
38883
|
const pending = /* @__PURE__ */ new Map();
|
|
38884
|
+
let firstHardError = null;
|
|
37938
38885
|
for (const item of saved) {
|
|
38886
|
+
if (firstHardError)
|
|
38887
|
+
break;
|
|
37939
38888
|
const task = this.vetIssue(item.issueUrl).then((candidate) => {
|
|
37940
38889
|
results.push({
|
|
37941
38890
|
issueUrl: item.issueUrl,
|
|
@@ -37947,15 +38896,19 @@ var OssScout = class {
|
|
|
37947
38896
|
viabilityScore: candidate.viabilityScore
|
|
37948
38897
|
});
|
|
37949
38898
|
}).catch((error48) => {
|
|
37950
|
-
|
|
37951
|
-
|
|
38899
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
38900
|
+
firstHardError ??= error48;
|
|
38901
|
+
return;
|
|
38902
|
+
}
|
|
38903
|
+
const status = getHttpStatusCode(error48);
|
|
38904
|
+
const isGone = status === 404 || status === 410;
|
|
37952
38905
|
results.push({
|
|
37953
38906
|
issueUrl: item.issueUrl,
|
|
37954
38907
|
repo: item.repo,
|
|
37955
38908
|
number: item.number,
|
|
37956
38909
|
title: item.title,
|
|
37957
38910
|
status: isGone ? "closed" : "error",
|
|
37958
|
-
errorMessage:
|
|
38911
|
+
errorMessage: errorMessage(error48)
|
|
37959
38912
|
});
|
|
37960
38913
|
}).finally(() => {
|
|
37961
38914
|
pending.delete(item.issueUrl);
|
|
@@ -37966,6 +38919,12 @@ var OssScout = class {
|
|
|
37966
38919
|
}
|
|
37967
38920
|
}
|
|
37968
38921
|
await Promise.allSettled(pending.values());
|
|
38922
|
+
if (firstHardError) {
|
|
38923
|
+
if (results.length > 0) {
|
|
38924
|
+
warn("scout", `vetList aborted mid-batch after ${results.length} result(s) \u2014 discarding partial results due to auth/rate-limit failure`);
|
|
38925
|
+
}
|
|
38926
|
+
throw firstHardError;
|
|
38927
|
+
}
|
|
37969
38928
|
const summary = {
|
|
37970
38929
|
total: results.length,
|
|
37971
38930
|
stillAvailable: results.filter((r) => r.status === "still_available").length,
|
|
@@ -38003,6 +38962,16 @@ var OssScout = class {
|
|
|
38003
38962
|
}
|
|
38004
38963
|
return [...repoCounts.entries()].sort((a, b) => b[1] - a[1]).map(([repo]) => repo);
|
|
38005
38964
|
}
|
|
38965
|
+
getReposWithOpenPRs() {
|
|
38966
|
+
const repoCounts = /* @__PURE__ */ new Map();
|
|
38967
|
+
for (const pr of this.state.openPRs ?? []) {
|
|
38968
|
+
const repo = extractRepoFromUrl(pr.url);
|
|
38969
|
+
if (repo) {
|
|
38970
|
+
repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
|
|
38971
|
+
}
|
|
38972
|
+
}
|
|
38973
|
+
return [...repoCounts.entries()].sort((a, b) => b[1] - a[1]).map(([repo]) => repo);
|
|
38974
|
+
}
|
|
38006
38975
|
getStarredRepos() {
|
|
38007
38976
|
return this.state.starredRepos;
|
|
38008
38977
|
}
|
|
@@ -38013,6 +38982,16 @@ var OssScout = class {
|
|
|
38013
38982
|
const score = this.state.repoScores[repo];
|
|
38014
38983
|
return score ? score.score : null;
|
|
38015
38984
|
}
|
|
38985
|
+
/**
|
|
38986
|
+
* Optional SLM pre-triage config read from preferences (oss-autopilot#1122).
|
|
38987
|
+
* Empty `model` disables the call; the vetter treats it as a no-op.
|
|
38988
|
+
*/
|
|
38989
|
+
getSLMTriageConfig() {
|
|
38990
|
+
return {
|
|
38991
|
+
model: this.state.preferences.slmTriageModel ?? "",
|
|
38992
|
+
host: this.state.preferences.slmTriageHost ?? ""
|
|
38993
|
+
};
|
|
38994
|
+
}
|
|
38016
38995
|
/** Get current preferences (read-only). */
|
|
38017
38996
|
getPreferences() {
|
|
38018
38997
|
return this.state.preferences;
|
|
@@ -38051,6 +39030,20 @@ var OssScout = class {
|
|
|
38051
39030
|
this.updateRepoScoreFromPRs(pr.repo);
|
|
38052
39031
|
this.dirty = true;
|
|
38053
39032
|
}
|
|
39033
|
+
/**
|
|
39034
|
+
* Record that a PR is currently open in this repo.
|
|
39035
|
+
* Open PRs signal active engagement even when nothing is merged yet.
|
|
39036
|
+
*/
|
|
39037
|
+
recordOpenPR(pr) {
|
|
39038
|
+
const existing = this.state.openPRs ?? [];
|
|
39039
|
+
if (existing.some((p) => p.url === pr.url))
|
|
39040
|
+
return;
|
|
39041
|
+
this.state.openPRs = [
|
|
39042
|
+
...existing,
|
|
39043
|
+
{ url: pr.url, title: pr.title, openedAt: pr.openedAt }
|
|
39044
|
+
];
|
|
39045
|
+
this.dirty = true;
|
|
39046
|
+
}
|
|
38054
39047
|
/**
|
|
38055
39048
|
* Update repo score with observed signals.
|
|
38056
39049
|
*/
|
|
@@ -38117,7 +39110,8 @@ var OssScout = class {
|
|
|
38117
39110
|
searchPriority: c.searchPriority,
|
|
38118
39111
|
firstSeenAt: prev?.firstSeenAt ?? now,
|
|
38119
39112
|
lastSeenAt: now,
|
|
38120
|
-
lastScore: c.viabilityScore
|
|
39113
|
+
lastScore: c.viabilityScore,
|
|
39114
|
+
horizon: "horizon" in c ? c.horizon : void 0
|
|
38121
39115
|
});
|
|
38122
39116
|
}
|
|
38123
39117
|
this.state.savedResults = [...existing.values()];
|
|
@@ -38302,6 +39296,37 @@ function registerTools(server, scout) {
|
|
|
38302
39296
|
}
|
|
38303
39297
|
}
|
|
38304
39298
|
);
|
|
39299
|
+
server.tool(
|
|
39300
|
+
"scout-features",
|
|
39301
|
+
"Surface feature-scoped contribution opportunities in repos where you have 3+ merged PRs",
|
|
39302
|
+
{
|
|
39303
|
+
maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
|
|
39304
|
+
anchorThreshold: external_exports3.number().int().min(1).max(50).optional().describe("Anchor threshold override (default 3)"),
|
|
39305
|
+
splitRatio: external_exports3.number().min(0).max(1).optional().describe("Quick-wins/bigger-bets split ratio (default 0.6)")
|
|
39306
|
+
},
|
|
39307
|
+
async ({ maxResults, anchorThreshold, splitRatio }) => {
|
|
39308
|
+
try {
|
|
39309
|
+
const result = await withTimeout(
|
|
39310
|
+
scout.features({
|
|
39311
|
+
count: maxResults ?? 10,
|
|
39312
|
+
anchorThreshold,
|
|
39313
|
+
splitRatio
|
|
39314
|
+
})
|
|
39315
|
+
);
|
|
39316
|
+
scout.saveResults([...result.quickWins, ...result.biggerBets]);
|
|
39317
|
+
await scout.checkpoint();
|
|
39318
|
+
return {
|
|
39319
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
39320
|
+
};
|
|
39321
|
+
} catch (err) {
|
|
39322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39323
|
+
return {
|
|
39324
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
39325
|
+
isError: true
|
|
39326
|
+
};
|
|
39327
|
+
}
|
|
39328
|
+
}
|
|
39329
|
+
);
|
|
38305
39330
|
server.tool(
|
|
38306
39331
|
"vet",
|
|
38307
39332
|
"Vet a specific GitHub issue for contribution viability",
|
|
@@ -38435,7 +39460,9 @@ function registerTools(server, scout) {
|
|
|
38435
39460
|
"minRepoScoreThreshold",
|
|
38436
39461
|
"interPhaseDelayMs",
|
|
38437
39462
|
"broadPhaseDelayMs",
|
|
38438
|
-
"skipBroadWhenSufficientResults"
|
|
39463
|
+
"skipBroadWhenSufficientResults",
|
|
39464
|
+
"featuresAnchorThreshold",
|
|
39465
|
+
"featuresSplitRatio"
|
|
38439
39466
|
]);
|
|
38440
39467
|
const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
|
|
38441
39468
|
server.tool(
|