@oss-scout/mcp 0.10.0 → 0.10.2

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.
@@ -3100,15 +3100,16 @@ var require_data = __commonJS({
3100
3100
  }
3101
3101
  });
3102
3102
 
3103
- // ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/utils.js
3103
+ // ../../node_modules/.pnpm/fast-uri@4.1.2/node_modules/fast-uri/lib/utils.js
3104
3104
  var require_utils = __commonJS({
3105
- "../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/utils.js"(exports2, module2) {
3105
+ "../../node_modules/.pnpm/fast-uri@4.1.2/node_modules/fast-uri/lib/utils.js"(exports2, module2) {
3106
3106
  "use strict";
3107
3107
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3108
3108
  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);
3109
3109
  var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3110
3110
  var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3111
3111
  var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3112
+ var isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/?]$/iu);
3112
3113
  function stringArrayToHexStripped(input) {
3113
3114
  let acc = "";
3114
3115
  let code = 0;
@@ -3251,7 +3252,7 @@ var require_utils = __commonJS({
3251
3252
  continue;
3252
3253
  }
3253
3254
  } else if (input[0] === "/") {
3254
- if (input[1] === "." || input[1] === "/") {
3255
+ if (input[1] === ".") {
3255
3256
  output.push("/");
3256
3257
  break;
3257
3258
  }
@@ -3333,10 +3334,30 @@ var require_utils = __commonJS({
3333
3334
  }
3334
3335
  return output;
3335
3336
  }
3337
+ var BYTE_HEX = new Array(256);
3338
+ {
3339
+ const HEX_DIGITS = "0123456789ABCDEF";
3340
+ for (let i = 0; i < 256; i++) {
3341
+ BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15];
3342
+ }
3343
+ }
3344
+ function isEscapeSafe(cp) {
3345
+ return cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp === 42 || cp === 43 || cp === 45 || cp === 46 || cp === 47 || cp === 64 || cp === 95;
3346
+ }
3347
+ function percentEncodeNonAscii(cp) {
3348
+ if (cp < 2048) {
3349
+ return BYTE_HEX[192 | cp >> 6] + BYTE_HEX[128 | cp & 63];
3350
+ }
3351
+ if (cp < 65536) {
3352
+ return BYTE_HEX[224 | cp >> 12] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
3353
+ }
3354
+ return BYTE_HEX[240 | cp >> 18] + BYTE_HEX[128 | cp >> 12 & 63] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
3355
+ }
3336
3356
  function normalizePathEncoding(input) {
3337
3357
  let output = "";
3338
3358
  for (let i = 0; i < input.length; i++) {
3339
- if (input[i] === "%" && i + 2 < input.length) {
3359
+ const ch = input[i];
3360
+ if (ch === "%" && i + 2 < input.length) {
3340
3361
  const hex3 = input.slice(i + 1, i + 3);
3341
3362
  if (isHexPair(hex3)) {
3342
3363
  const normalizedHex = hex3.toUpperCase();
@@ -3350,10 +3371,66 @@ var require_utils = __commonJS({
3350
3371
  continue;
3351
3372
  }
3352
3373
  }
3353
- if (isPathCharacter(input[i])) {
3354
- output += input[i];
3374
+ if (isPathCharacter(ch)) {
3375
+ output += ch;
3355
3376
  } else {
3356
- output += escape(input[i]);
3377
+ const code = input.charCodeAt(i);
3378
+ if (code < 128) {
3379
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3380
+ } else if (code < 55296 || code > 57343) {
3381
+ output += percentEncodeNonAscii(code);
3382
+ } else if (code <= 56319 && i + 1 < input.length) {
3383
+ const low = input.charCodeAt(i + 1);
3384
+ if (low >= 56320 && low <= 57343) {
3385
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3386
+ i++;
3387
+ } else {
3388
+ output += percentEncodeNonAscii(65533);
3389
+ }
3390
+ } else {
3391
+ output += percentEncodeNonAscii(65533);
3392
+ }
3393
+ }
3394
+ }
3395
+ return output;
3396
+ }
3397
+ function normalizeQueryFragmentEncoding(input) {
3398
+ let output = "";
3399
+ for (let i = 0; i < input.length; i++) {
3400
+ const ch = input[i];
3401
+ if (ch === "%" && i + 2 < input.length) {
3402
+ const hex3 = input.slice(i + 1, i + 3);
3403
+ if (isHexPair(hex3)) {
3404
+ const normalizedHex = hex3.toUpperCase();
3405
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3406
+ if (isUnreserved(decoded)) {
3407
+ output += decoded;
3408
+ } else {
3409
+ output += "%" + normalizedHex;
3410
+ }
3411
+ i += 2;
3412
+ continue;
3413
+ }
3414
+ }
3415
+ if (isQueryFragmentCharacter(ch)) {
3416
+ output += ch;
3417
+ } else {
3418
+ const code = input.charCodeAt(i);
3419
+ if (code < 128) {
3420
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3421
+ } else if (code < 55296 || code > 57343) {
3422
+ output += percentEncodeNonAscii(code);
3423
+ } else if (code <= 56319 && i + 1 < input.length) {
3424
+ const low = input.charCodeAt(i + 1);
3425
+ if (low >= 56320 && low <= 57343) {
3426
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3427
+ i++;
3428
+ } else {
3429
+ output += percentEncodeNonAscii(65533);
3430
+ }
3431
+ } else {
3432
+ output += percentEncodeNonAscii(65533);
3433
+ }
3357
3434
  }
3358
3435
  }
3359
3436
  return output;
@@ -3361,7 +3438,8 @@ var require_utils = __commonJS({
3361
3438
  function escapePreservingEscapes(input) {
3362
3439
  let output = "";
3363
3440
  for (let i = 0; i < input.length; i++) {
3364
- if (input[i] === "%" && i + 2 < input.length) {
3441
+ const ch = input[i];
3442
+ if (ch === "%" && i + 2 < input.length) {
3365
3443
  const hex3 = input.slice(i + 1, i + 3);
3366
3444
  if (isHexPair(hex3)) {
3367
3445
  output += "%" + hex3.toUpperCase();
@@ -3369,7 +3447,22 @@ var require_utils = __commonJS({
3369
3447
  continue;
3370
3448
  }
3371
3449
  }
3372
- output += escape(input[i]);
3450
+ const code = input.charCodeAt(i);
3451
+ if (code < 128) {
3452
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3453
+ } else if (code < 55296 || code > 57343) {
3454
+ output += percentEncodeNonAscii(code);
3455
+ } else if (code <= 56319 && i + 1 < input.length) {
3456
+ const low = input.charCodeAt(i + 1);
3457
+ if (low >= 56320 && low <= 57343) {
3458
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3459
+ i++;
3460
+ } else {
3461
+ output += percentEncodeNonAscii(65533);
3462
+ }
3463
+ } else {
3464
+ output += percentEncodeNonAscii(65533);
3465
+ }
3373
3466
  }
3374
3467
  return output;
3375
3468
  }
@@ -3403,6 +3496,7 @@ var require_utils = __commonJS({
3403
3496
  reescapeHostDelimiters,
3404
3497
  normalizePercentEncoding,
3405
3498
  normalizePathEncoding,
3499
+ normalizeQueryFragmentEncoding,
3406
3500
  escapePreservingEscapes,
3407
3501
  removeDotSegments,
3408
3502
  isIPv4,
@@ -3413,9 +3507,9 @@ var require_utils = __commonJS({
3413
3507
  }
3414
3508
  });
3415
3509
 
3416
- // ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/schemes.js
3510
+ // ../../node_modules/.pnpm/fast-uri@4.1.2/node_modules/fast-uri/lib/schemes.js
3417
3511
  var require_schemes = __commonJS({
3418
- "../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/schemes.js"(exports2, module2) {
3512
+ "../../node_modules/.pnpm/fast-uri@4.1.2/node_modules/fast-uri/lib/schemes.js"(exports2, module2) {
3419
3513
  "use strict";
3420
3514
  var { isUUID } = require_utils();
3421
3515
  var URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
@@ -3623,11 +3717,11 @@ var require_schemes = __commonJS({
3623
3717
  }
3624
3718
  });
3625
3719
 
3626
- // ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/index.js
3720
+ // ../../node_modules/.pnpm/fast-uri@4.1.2/node_modules/fast-uri/index.js
3627
3721
  var require_fast_uri = __commonJS({
3628
- "../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/index.js"(exports2, module2) {
3722
+ "../../node_modules/.pnpm/fast-uri@4.1.2/node_modules/fast-uri/index.js"(exports2, module2) {
3629
3723
  "use strict";
3630
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3724
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, normalizeQueryFragmentEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3631
3725
  var { SCHEMES, getSchemeHandler } = require_schemes();
3632
3726
  function normalize(uri, options) {
3633
3727
  if (typeof uri === "string") {
@@ -3641,7 +3735,12 @@ var require_fast_uri = __commonJS({
3641
3735
  }
3642
3736
  function resolve(baseURI, relativeURI, options) {
3643
3737
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3644
- const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
3738
+ const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3739
+ const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
3740
+ if (baseMalformed || relativeMalformed) {
3741
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
3742
+ }
3743
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
3645
3744
  schemelessOptions.skipEscape = true;
3646
3745
  return serialize(resolved, schemelessOptions);
3647
3746
  }
@@ -3766,6 +3865,8 @@ var require_fast_uri = __commonJS({
3766
3865
  return uriTokens.join("");
3767
3866
  }
3768
3867
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3868
+ var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3869
+ var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
3769
3870
  function getParseError(parsed, matches) {
3770
3871
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3771
3872
  return 'URI path must start with "/" when authority is present.';
@@ -3795,9 +3896,28 @@ var require_fast_uri = __commonJS({
3795
3896
  uri = "//" + uri;
3796
3897
  }
3797
3898
  }
3899
+ const authorityMatch = uri.match(AUTHORITY_PREFIX);
3900
+ if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
3901
+ parsed.error = "URI authority must not contain a literal backslash.";
3902
+ malformedAuthorityOrPort = true;
3903
+ }
3904
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
3905
+ if (introducerMatch !== null) {
3906
+ const region = introducerMatch[1];
3907
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
3908
+ if (normalizedRegion.length >= 2) {
3909
+ if (normalizedRegion.slice(0, 2) !== "//") {
3910
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
3911
+ malformedAuthorityOrPort = true;
3912
+ } else if (region.length !== normalizedRegion.length) {
3913
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
3914
+ malformedAuthorityOrPort = true;
3915
+ }
3916
+ }
3917
+ }
3798
3918
  const matches = uri.match(URI_PARSE);
3799
3919
  if (matches) {
3800
- parsed.scheme = matches[1];
3920
+ parsed.scheme = matches[1] === void 0 ? void 0 : matches[1].toLowerCase();
3801
3921
  parsed.userinfo = matches[3];
3802
3922
  parsed.host = matches[4];
3803
3923
  parsed.port = parseInt(matches[5], 10);
@@ -3838,7 +3958,7 @@ var require_fast_uri = __commonJS({
3838
3958
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
3839
3959
  if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
3840
3960
  try {
3841
- parsed.host = URL.domainToASCII(parsed.host.toLowerCase());
3961
+ parsed.host = new URL("http://" + parsed.host).hostname;
3842
3962
  } catch (e) {
3843
3963
  parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
3844
3964
  }
@@ -3856,12 +3976,11 @@ var require_fast_uri = __commonJS({
3856
3976
  if (parsed.path) {
3857
3977
  parsed.path = normalizePathEncoding(parsed.path);
3858
3978
  }
3979
+ if (parsed.query) {
3980
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query);
3981
+ }
3859
3982
  if (parsed.fragment) {
3860
- try {
3861
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3862
- } catch {
3863
- parsed.error = parsed.error || "URI malformed";
3864
- }
3983
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
3865
3984
  }
3866
3985
  }
3867
3986
  if (schemeHandler && schemeHandler.parse) {
@@ -7356,11 +7475,11 @@ var require_light = __commonJS({
7356
7475
  return this._onFailure(error48, eventInfo, clearGlobalState, run, free);
7357
7476
  }
7358
7477
  async _onFailure(error48, eventInfo, clearGlobalState, run, free) {
7359
- var retry, retryAfter;
7478
+ var retry2, retryAfter;
7360
7479
  if (clearGlobalState()) {
7361
- retry = await this.Events.trigger("failed", error48, eventInfo);
7362
- if (retry != null) {
7363
- retryAfter = ~~retry;
7480
+ retry2 = await this.Events.trigger("failed", error48, eventInfo);
7481
+ if (retry2 != null) {
7482
+ retryAfter = ~~retry2;
7364
7483
  this.Events.trigger("retry", `Retrying ${this.options.id} after ${retryAfter} ms`, eventInfo);
7365
7484
  this.retryCount++;
7366
7485
  return run(retryAfter);
@@ -7875,7 +7994,7 @@ var require_light = __commonJS({
7875
7994
  var Batcher_1 = Batcher;
7876
7995
  var require$$4$1 = () => console.log("You must import the full version of Bottleneck in order to use this feature.");
7877
7996
  var require$$8 = getCjsExportFromNamespace(version$2);
7878
- var Bottleneck, DEFAULT_PRIORITY$1, Events$4, Job$1, LocalDatastore$1, NUM_PRIORITIES$1, Queues$1, RedisDatastore$1, States$1, Sync$1, parser$5, splice = [].splice;
7997
+ var Bottleneck2, DEFAULT_PRIORITY$1, Events$4, Job$1, LocalDatastore$1, NUM_PRIORITIES$1, Queues$1, RedisDatastore$1, States$1, Sync$1, parser$5, splice = [].splice;
7879
7998
  NUM_PRIORITIES$1 = 10;
7880
7999
  DEFAULT_PRIORITY$1 = 5;
7881
8000
  parser$5 = parser;
@@ -7886,8 +8005,8 @@ var require_light = __commonJS({
7886
8005
  Events$4 = Events_1;
7887
8006
  States$1 = States_1;
7888
8007
  Sync$1 = Sync_1;
7889
- Bottleneck = (function() {
7890
- class Bottleneck2 {
8008
+ Bottleneck2 = (function() {
8009
+ class Bottleneck3 {
7891
8010
  constructor(options = {}, ...invalid) {
7892
8011
  var storeInstanceOptions, storeOptions;
7893
8012
  this._addToQueue = this._addToQueue.bind(this);
@@ -7909,7 +8028,7 @@ var require_light = __commonJS({
7909
8028
  storeInstanceOptions = parser$5.load(options, this.localStoreDefaults, {});
7910
8029
  return new LocalDatastore$1(this, storeOptions, storeInstanceOptions);
7911
8030
  } else {
7912
- throw new Bottleneck2.prototype.BottleneckError(`Invalid datastore type: ${this.datastore}`);
8031
+ throw new Bottleneck3.prototype.BottleneckError(`Invalid datastore type: ${this.datastore}`);
7913
8032
  }
7914
8033
  }).call(this);
7915
8034
  this._queues.on("leftzero", () => {
@@ -7923,7 +8042,7 @@ var require_light = __commonJS({
7923
8042
  }
7924
8043
  _validateOptions(options, invalid) {
7925
8044
  if (!(options != null && typeof options === "object" && invalid.length === 0)) {
7926
- throw new Bottleneck2.prototype.BottleneckError("Bottleneck v2 takes a single object argument. Refer to https://github.com/SGrondin/bottleneck#upgrading-to-v2 if you're upgrading from Bottleneck v1.");
8045
+ throw new Bottleneck3.prototype.BottleneckError("Bottleneck v2 takes a single object argument. Refer to https://github.com/SGrondin/bottleneck#upgrading-to-v2 if you're upgrading from Bottleneck v1.");
7927
8046
  }
7928
8047
  }
7929
8048
  ready() {
@@ -8120,10 +8239,10 @@ var require_light = __commonJS({
8120
8239
  return waitForExecuting(1);
8121
8240
  });
8122
8241
  this._receive = function(job) {
8123
- return job._reject(new Bottleneck2.prototype.BottleneckError(options.enqueueErrorMessage));
8242
+ return job._reject(new Bottleneck3.prototype.BottleneckError(options.enqueueErrorMessage));
8124
8243
  };
8125
8244
  this.stop = () => {
8126
- return this.Promise.reject(new Bottleneck2.prototype.BottleneckError("stop() has already been called"));
8245
+ return this.Promise.reject(new Bottleneck3.prototype.BottleneckError("stop() has already been called"));
8127
8246
  };
8128
8247
  return done;
8129
8248
  }
@@ -8142,11 +8261,11 @@ var require_light = __commonJS({
8142
8261
  job.doDrop();
8143
8262
  return true;
8144
8263
  } else if (reachedHWM) {
8145
- shifted = strategy === Bottleneck2.prototype.strategy.LEAK ? this._queues.shiftLastFrom(options.priority) : strategy === Bottleneck2.prototype.strategy.OVERFLOW_PRIORITY ? this._queues.shiftLastFrom(options.priority + 1) : strategy === Bottleneck2.prototype.strategy.OVERFLOW ? job : void 0;
8264
+ shifted = strategy === Bottleneck3.prototype.strategy.LEAK ? this._queues.shiftLastFrom(options.priority) : strategy === Bottleneck3.prototype.strategy.OVERFLOW_PRIORITY ? this._queues.shiftLastFrom(options.priority + 1) : strategy === Bottleneck3.prototype.strategy.OVERFLOW ? job : void 0;
8146
8265
  if (shifted != null) {
8147
8266
  shifted.doDrop();
8148
8267
  }
8149
- if (shifted == null || strategy === Bottleneck2.prototype.strategy.OVERFLOW) {
8268
+ if (shifted == null || strategy === Bottleneck3.prototype.strategy.OVERFLOW) {
8150
8269
  if (shifted == null) {
8151
8270
  job.doDrop();
8152
8271
  }
@@ -8160,7 +8279,7 @@ var require_light = __commonJS({
8160
8279
  }
8161
8280
  _receive(job) {
8162
8281
  if (this._states.jobStatus(job.options.id) != null) {
8163
- job._reject(new Bottleneck2.prototype.BottleneckError(`A job with the same id already exists (id=${job.options.id})`));
8282
+ job._reject(new Bottleneck3.prototype.BottleneckError(`A job with the same id already exists (id=${job.options.id})`));
8164
8283
  return false;
8165
8284
  } else {
8166
8285
  job.doReceive();
@@ -8230,31 +8349,31 @@ var require_light = __commonJS({
8230
8349
  return this._store.__incrementReservoir__(incr);
8231
8350
  }
8232
8351
  }
8233
- Bottleneck2.default = Bottleneck2;
8234
- Bottleneck2.Events = Events$4;
8235
- Bottleneck2.version = Bottleneck2.prototype.version = require$$8.version;
8236
- Bottleneck2.strategy = Bottleneck2.prototype.strategy = {
8352
+ Bottleneck3.default = Bottleneck3;
8353
+ Bottleneck3.Events = Events$4;
8354
+ Bottleneck3.version = Bottleneck3.prototype.version = require$$8.version;
8355
+ Bottleneck3.strategy = Bottleneck3.prototype.strategy = {
8237
8356
  LEAK: 1,
8238
8357
  OVERFLOW: 2,
8239
8358
  OVERFLOW_PRIORITY: 4,
8240
8359
  BLOCK: 3
8241
8360
  };
8242
- Bottleneck2.BottleneckError = Bottleneck2.prototype.BottleneckError = BottleneckError_1;
8243
- Bottleneck2.Group = Bottleneck2.prototype.Group = Group_1;
8244
- Bottleneck2.RedisConnection = Bottleneck2.prototype.RedisConnection = require$$2;
8245
- Bottleneck2.IORedisConnection = Bottleneck2.prototype.IORedisConnection = require$$3;
8246
- Bottleneck2.Batcher = Bottleneck2.prototype.Batcher = Batcher_1;
8247
- Bottleneck2.prototype.jobDefaults = {
8361
+ Bottleneck3.BottleneckError = Bottleneck3.prototype.BottleneckError = BottleneckError_1;
8362
+ Bottleneck3.Group = Bottleneck3.prototype.Group = Group_1;
8363
+ Bottleneck3.RedisConnection = Bottleneck3.prototype.RedisConnection = require$$2;
8364
+ Bottleneck3.IORedisConnection = Bottleneck3.prototype.IORedisConnection = require$$3;
8365
+ Bottleneck3.Batcher = Bottleneck3.prototype.Batcher = Batcher_1;
8366
+ Bottleneck3.prototype.jobDefaults = {
8248
8367
  priority: DEFAULT_PRIORITY$1,
8249
8368
  weight: 1,
8250
8369
  expiration: null,
8251
8370
  id: "<no-id>"
8252
8371
  };
8253
- Bottleneck2.prototype.storeDefaults = {
8372
+ Bottleneck3.prototype.storeDefaults = {
8254
8373
  maxConcurrent: null,
8255
8374
  minTime: 0,
8256
8375
  highWater: null,
8257
- strategy: Bottleneck2.prototype.strategy.LEAK,
8376
+ strategy: Bottleneck3.prototype.strategy.LEAK,
8258
8377
  penalty: null,
8259
8378
  reservoir: null,
8260
8379
  reservoirRefreshInterval: null,
@@ -8263,12 +8382,12 @@ var require_light = __commonJS({
8263
8382
  reservoirIncreaseAmount: null,
8264
8383
  reservoirIncreaseMaximum: null
8265
8384
  };
8266
- Bottleneck2.prototype.localStoreDefaults = {
8385
+ Bottleneck3.prototype.localStoreDefaults = {
8267
8386
  Promise,
8268
8387
  timeout: null,
8269
8388
  heartbeatInterval: 250
8270
8389
  };
8271
- Bottleneck2.prototype.redisStoreDefaults = {
8390
+ Bottleneck3.prototype.redisStoreDefaults = {
8272
8391
  Promise,
8273
8392
  timeout: null,
8274
8393
  heartbeatInterval: 5e3,
@@ -8279,7 +8398,7 @@ var require_light = __commonJS({
8279
8398
  clearDatastore: false,
8280
8399
  connection: null
8281
8400
  };
8282
- Bottleneck2.prototype.instanceDefaults = {
8401
+ Bottleneck3.prototype.instanceDefaults = {
8283
8402
  datastore: "local",
8284
8403
  connection: null,
8285
8404
  id: "<no-id>",
@@ -8287,14 +8406,14 @@ var require_light = __commonJS({
8287
8406
  trackDoneStatus: false,
8288
8407
  Promise
8289
8408
  };
8290
- Bottleneck2.prototype.stopDefaults = {
8409
+ Bottleneck3.prototype.stopDefaults = {
8291
8410
  enqueueErrorMessage: "This limiter has been stopped and cannot accept new jobs.",
8292
8411
  dropWaitingJobs: true,
8293
8412
  dropErrorMessage: "This limiter has been stopped."
8294
8413
  };
8295
- return Bottleneck2;
8414
+ return Bottleneck3;
8296
8415
  }).call(commonjsGlobal);
8297
- var Bottleneck_1 = Bottleneck;
8416
+ var Bottleneck_1 = Bottleneck2;
8298
8417
  var lib = Bottleneck_1;
8299
8418
  return lib;
8300
8419
  }));
@@ -35311,30 +35430,30 @@ function routeMatcher(paths) {
35311
35430
  var regex = routeMatcher(triggers_notification_paths_default);
35312
35431
  var triggersNotification = regex.test.bind(regex);
35313
35432
  var groups = {};
35314
- var createGroups = function(Bottleneck, common) {
35315
- groups.global = new Bottleneck.Group({
35433
+ var createGroups = function(Bottleneck2, common) {
35434
+ groups.global = new Bottleneck2.Group({
35316
35435
  id: "octokit-global",
35317
35436
  maxConcurrent: 10,
35318
35437
  ...common
35319
35438
  });
35320
- groups.auth = new Bottleneck.Group({
35439
+ groups.auth = new Bottleneck2.Group({
35321
35440
  id: "octokit-auth",
35322
35441
  maxConcurrent: 1,
35323
35442
  ...common
35324
35443
  });
35325
- groups.search = new Bottleneck.Group({
35444
+ groups.search = new Bottleneck2.Group({
35326
35445
  id: "octokit-search",
35327
35446
  maxConcurrent: 1,
35328
35447
  minTime: 2e3,
35329
35448
  ...common
35330
35449
  });
35331
- groups.write = new Bottleneck.Group({
35450
+ groups.write = new Bottleneck2.Group({
35332
35451
  id: "octokit-write",
35333
35452
  maxConcurrent: 1,
35334
35453
  minTime: 1e3,
35335
35454
  ...common
35336
35455
  });
35337
- groups.notifications = new Bottleneck.Group({
35456
+ groups.notifications = new Bottleneck2.Group({
35338
35457
  id: "octokit-notifications",
35339
35458
  maxConcurrent: 1,
35340
35459
  minTime: 3e3,
@@ -35344,7 +35463,7 @@ var createGroups = function(Bottleneck, common) {
35344
35463
  function throttling(octokit, octokitOptions) {
35345
35464
  const {
35346
35465
  enabled = true,
35347
- Bottleneck = import_light.default,
35466
+ Bottleneck: Bottleneck2 = import_light.default,
35348
35467
  id = "no-id",
35349
35468
  timeout = 1e3 * 60 * 2,
35350
35469
  // Redis TTL: 2 minutes
@@ -35358,7 +35477,7 @@ function throttling(octokit, octokitOptions) {
35358
35477
  common.connection = connection;
35359
35478
  }
35360
35479
  if (groups.global == null) {
35361
- createGroups(Bottleneck, common);
35480
+ createGroups(Bottleneck2, common);
35362
35481
  }
35363
35482
  const state = Object.assign(
35364
35483
  {
@@ -35366,7 +35485,7 @@ function throttling(octokit, octokitOptions) {
35366
35485
  triggersNotification,
35367
35486
  fallbackSecondaryRateRetryAfter: 60,
35368
35487
  retryAfterBaseValue: 1e3,
35369
- retryLimiter: new Bottleneck(),
35488
+ retryLimiter: new Bottleneck2(),
35370
35489
  id,
35371
35490
  ...groups
35372
35491
  },
@@ -35386,7 +35505,7 @@ function throttling(octokit, octokitOptions) {
35386
35505
  `);
35387
35506
  }
35388
35507
  const events = {};
35389
- const emitter = new Bottleneck.Events(events);
35508
+ const emitter = new Bottleneck2.Events(events);
35390
35509
  events.on("secondary-limit", state.onSecondaryRateLimit);
35391
35510
  events.on("rate-limit", state.onRateLimit);
35392
35511
  events.on(
@@ -35449,6 +35568,80 @@ function throttling(octokit, octokitOptions) {
35449
35568
  throttling.VERSION = VERSION9;
35450
35569
  throttling.triggersNotification = triggersNotification;
35451
35570
 
35571
+ // ../../node_modules/.pnpm/@octokit+plugin-retry@8.1.0_@octokit+core@7.0.6/node_modules/@octokit/plugin-retry/dist-bundle/index.js
35572
+ var import_light2 = __toESM(require_light(), 1);
35573
+ var VERSION10 = "0.0.0-development";
35574
+ function isRequestError(error48) {
35575
+ return error48.request !== void 0;
35576
+ }
35577
+ async function errorRequest(state, octokit, error48, options) {
35578
+ if (!isRequestError(error48) || !error48?.request.request) {
35579
+ throw error48;
35580
+ }
35581
+ if (error48.status >= 400 && !state.doNotRetry.includes(error48.status)) {
35582
+ const retries = options.request.retries != null ? options.request.retries : state.retries;
35583
+ const retryAfter = Math.pow((options.request.retryCount || 0) + 1, 2);
35584
+ throw octokit.retry.retryRequest(error48, retries, retryAfter);
35585
+ }
35586
+ throw error48;
35587
+ }
35588
+ async function wrapRequest2(state, octokit, request2, options) {
35589
+ const limiter = new import_light2.default();
35590
+ limiter.on("failed", function(error48, info2) {
35591
+ const maxRetries = ~~error48.request.request?.retries;
35592
+ const after = ~~error48.request.request?.retryAfter;
35593
+ options.request.retryCount = info2.retryCount + 1;
35594
+ if (maxRetries > info2.retryCount) {
35595
+ return after * state.retryAfterBaseValue;
35596
+ }
35597
+ });
35598
+ return limiter.schedule(
35599
+ requestWithGraphqlErrorHandling.bind(null, state, octokit, request2),
35600
+ options
35601
+ );
35602
+ }
35603
+ async function requestWithGraphqlErrorHandling(state, octokit, request2, options) {
35604
+ const response = await request2(options);
35605
+ if (response.data && response.data.errors && response.data.errors.length > 0 && /Something went wrong while executing your query/.test(
35606
+ response.data.errors[0].message
35607
+ )) {
35608
+ const error48 = new RequestError(response.data.errors[0].message, 500, {
35609
+ request: options,
35610
+ response
35611
+ });
35612
+ return errorRequest(state, octokit, error48, options);
35613
+ }
35614
+ return response;
35615
+ }
35616
+ function retry(octokit, octokitOptions) {
35617
+ const state = Object.assign(
35618
+ {
35619
+ enabled: true,
35620
+ retryAfterBaseValue: 1e3,
35621
+ doNotRetry: [400, 401, 403, 404, 410, 422, 451],
35622
+ retries: 3
35623
+ },
35624
+ octokitOptions.retry
35625
+ );
35626
+ const retryPlugin = {
35627
+ retry: {
35628
+ retryRequest: (error48, retries, retryAfter) => {
35629
+ error48.request.request = Object.assign({}, error48.request.request, {
35630
+ retries,
35631
+ retryAfter
35632
+ });
35633
+ return error48;
35634
+ }
35635
+ }
35636
+ };
35637
+ if (state.enabled) {
35638
+ octokit.hook.error("request", errorRequest.bind(null, state, retryPlugin));
35639
+ octokit.hook.wrap("request", wrapRequest2.bind(null, state, retryPlugin));
35640
+ }
35641
+ return retryPlugin;
35642
+ }
35643
+ retry.VERSION = VERSION10;
35644
+
35452
35645
  // ../core/dist/core/logger.js
35453
35646
  var LEVEL_RANK = {
35454
35647
  silent: 0,
@@ -35484,7 +35677,7 @@ function warn(module2, message, ...args) {
35484
35677
 
35485
35678
  // ../core/dist/core/github.js
35486
35679
  var MODULE = "github";
35487
- var ThrottledOctokit = Octokit2.plugin(throttling);
35680
+ var ThrottledOctokit = Octokit2.plugin(throttling, retry);
35488
35681
  var _octokit = null;
35489
35682
  var _currentToken = null;
35490
35683
  function formatResetTime(date5) {
@@ -35552,8 +35745,10 @@ var OssScoutError = class extends Error {
35552
35745
  }
35553
35746
  };
35554
35747
  var ValidationError = class extends OssScoutError {
35555
- constructor(message) {
35748
+ strategiesUsed;
35749
+ constructor(message, strategiesUsed) {
35556
35750
  super(message, "VALIDATION_ERROR");
35751
+ this.strategiesUsed = strategiesUsed;
35557
35752
  this.name = "ValidationError";
35558
35753
  }
35559
35754
  };
@@ -35567,6 +35762,12 @@ function getHttpStatusCode(error48) {
35567
35762
  }
35568
35763
  return void 0;
35569
35764
  }
35765
+ function isAuthError(error48) {
35766
+ const status = getHttpStatusCode(error48);
35767
+ if (status === 401)
35768
+ return true;
35769
+ return status === 403 && !isRateLimitError(error48);
35770
+ }
35570
35771
  function isRateLimitError(error48) {
35571
35772
  const status = getHttpStatusCode(error48);
35572
35773
  if (status === 429)
@@ -35702,6 +35903,13 @@ var SearchBudgetTracker = class {
35702
35903
  totalCalls = 0;
35703
35904
  /** Calls made since the last known quota reset (external accounting). */
35704
35905
  callsSinceReset = 0;
35906
+ /**
35907
+ * Slots handed out by waitForBudget() but not yet recorded. Without this,
35908
+ * concurrent callers (vetting runs 3 tasks) could all pass waitForBudget()
35909
+ * on an effective budget of 1 before any recordCall() landed, overshooting
35910
+ * the external quota into real 429s (#287).
35911
+ */
35912
+ reservedCalls = 0;
35705
35913
  /**
35706
35914
  * Initialize with pre-flight rate limit data from GitHub.
35707
35915
  */
@@ -35711,12 +35919,17 @@ var SearchBudgetTracker = class {
35711
35919
  this.callTimestamps = [];
35712
35920
  this.totalCalls = 0;
35713
35921
  this.callsSinceReset = 0;
35922
+ this.reservedCalls = 0;
35714
35923
  debug(MODULE3, `Initialized: ${remaining} remaining, resets at ${new Date(this.resetAt).toLocaleTimeString()}`);
35715
35924
  }
35716
35925
  /**
35717
- * Record that a Search API call was just made.
35926
+ * Record that a Search API call was just made. Releases the reservation
35927
+ * taken by the preceding waitForBudget() (every call site pairs them, with
35928
+ * recordCall in a finally).
35718
35929
  */
35719
35930
  recordCall() {
35931
+ if (this.reservedCalls > 0)
35932
+ this.reservedCalls--;
35720
35933
  this.callTimestamps.push(Date.now());
35721
35934
  this.totalCalls++;
35722
35935
  this.callsSinceReset++;
@@ -35763,8 +35976,8 @@ var SearchBudgetTracker = class {
35763
35976
  */
35764
35977
  getEffectiveBudget() {
35765
35978
  this.refreshExternalBudget();
35766
- const localBudget = EFFECTIVE_BUDGET - this.callTimestamps.length;
35767
- const externalBudget = this.knownRemaining - this.callsSinceReset;
35979
+ const localBudget = EFFECTIVE_BUDGET - this.callTimestamps.length - this.reservedCalls;
35980
+ const externalBudget = this.knownRemaining - this.callsSinceReset - this.reservedCalls;
35768
35981
  return Math.max(0, Math.min(localBudget, externalBudget));
35769
35982
  }
35770
35983
  /**
@@ -35775,7 +35988,9 @@ var SearchBudgetTracker = class {
35775
35988
  return this.getEffectiveBudget() >= n;
35776
35989
  }
35777
35990
  /**
35778
- * Wait if necessary to stay within the Search API rate limit.
35991
+ * Wait if necessary to stay within the Search API rate limit, then reserve
35992
+ * one call slot. The reservation is released by the paired recordCall(), so
35993
+ * concurrent callers cannot all claim the same last remaining slot (#287).
35779
35994
  * If the sliding window is at capacity, sleeps until the oldest
35780
35995
  * call ages out of the window.
35781
35996
  */
@@ -35783,16 +35998,22 @@ var SearchBudgetTracker = class {
35783
35998
  while (true) {
35784
35999
  this.pruneOldTimestamps();
35785
36000
  if (this.getEffectiveBudget() > 0) {
36001
+ this.reservedCalls++;
35786
36002
  return;
35787
36003
  }
35788
36004
  const oldestInWindow = this.callTimestamps[0];
35789
36005
  if (!oldestInWindow) {
36006
+ if (this.reservedCalls > 0) {
36007
+ await sleep(250);
36008
+ continue;
36009
+ }
35790
36010
  const untilReset = this.resetAt - Date.now();
35791
36011
  if (this.resetAt > 0 && untilReset > 0) {
35792
36012
  debug(MODULE3, `External quota exhausted, waiting ${untilReset}ms for reset`);
35793
36013
  await sleep(untilReset + 100);
35794
36014
  continue;
35795
36015
  }
36016
+ this.reservedCalls++;
35796
36017
  return;
35797
36018
  }
35798
36019
  const waitUntil = oldestInWindow + SEARCH_WINDOW_MS;
@@ -36018,7 +36239,11 @@ var ScoutPreferencesSchema = external_exports3.looseObject({
36018
36239
  // Soft boost for candidates whose issue labels match one of these types,
36019
36240
  // case-insensitive (e.g. "bug", "good first issue") (#168).
36020
36241
  boostIssueTypes: external_exports3.array(external_exports3.string()).default([]),
36021
- broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
36242
+ // Broad-phase cooldown. Now that the broad phase runs on GraphQL (points
36243
+ // bucket, 5000/hr) rather than the REST Search bucket (30/min), the long
36244
+ // secondary-rate-limit cooldown is no longer needed; defaults to 0 (no extra
36245
+ // pause). Retained as a knob for hosts that still want to throttle the phase.
36246
+ broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(0),
36022
36247
  /**
36023
36248
  * Skip the expensive broad phase once this many candidates were found by
36024
36249
  * the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
@@ -36081,7 +36306,20 @@ var ScoutStateSchema = external_exports3.looseObject({
36081
36306
  preferencesUpdatedAt: external_exports3.string().optional(),
36082
36307
  lastSearchAt: external_exports3.string().optional(),
36083
36308
  lastRunAt: external_exports3.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
36084
- gistId: external_exports3.string().optional()
36309
+ gistId: external_exports3.string().optional(),
36310
+ /**
36311
+ * Diversity cursor for Phase 2's broad search (#249 follow-up). Without
36312
+ * this, every run started the language-variant fan-out at index 0, so the
36313
+ * broad phase only ever queried the first configured language before
36314
+ * moving on. Not a rate-limit mechanism — purely rotates which language
36315
+ * variant leads each run. `languageOffset` advances (no modulo; the
36316
+ * consumer wraps at use time so a shrinking language list never needs
36317
+ * clamping here).
36318
+ */
36319
+ searchRotation: external_exports3.looseObject({
36320
+ languageOffset: external_exports3.number().int().min(0).default(0),
36321
+ lastRotatedAt: external_exports3.string().optional()
36322
+ }).default(() => ({ languageOffset: 0 }))
36085
36323
  });
36086
36324
  function parseScoutState(raw) {
36087
36325
  return ScoutStateSchema.parse(raw);
@@ -36734,9 +36972,12 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
36734
36972
  }
36735
36973
  }
36736
36974
  var MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1e3;
36975
+ function mergedPRsCacheKey(owner, repo) {
36976
+ return versionedCacheKey(`merged-prs:${owner}/${repo}`);
36977
+ }
36737
36978
  async function checkUserMergedPRsInRepo(octokit, owner, repo, tracker = getSearchBudgetTracker()) {
36738
36979
  const cache = getHttpCache();
36739
- const cacheKey = versionedCacheKey(`merged-prs:${owner}/${repo}`);
36980
+ const cacheKey = mergedPRsCacheKey(owner, repo);
36740
36981
  return withInflightDedup(cache, cacheKey, async () => {
36741
36982
  const cached2 = cache.getIfFresh(cacheKey, MERGED_PR_CACHE_TTL_MS);
36742
36983
  if (cached2 != null && typeof cached2 === "number") {
@@ -36781,6 +37022,17 @@ async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount)
36781
37022
  });
36782
37023
  recentComments.push(...response.data);
36783
37024
  }
37025
+ const lastFetched = recentComments.length % PER_PAGE;
37026
+ if (recentComments.length > 0 && lastFetched === 0) {
37027
+ const response = await octokit.issues.listComments({
37028
+ owner,
37029
+ repo,
37030
+ issue_number: issueNumber,
37031
+ per_page: PER_PAGE,
37032
+ page: lastPage + 1
37033
+ });
37034
+ recentComments.push(...response.data);
37035
+ }
36784
37036
  for (const comment of recentComments) {
36785
37037
  if (commentClaimsIssue(comment.body || "")) {
36786
37038
  return { passed: false };
@@ -36813,7 +37065,7 @@ function analyzeRequirements(body) {
36813
37065
  var MODULE6 = "probe-repo-file";
36814
37066
  async function probeRepoFile(octokit, owner, repo, path5) {
36815
37067
  try {
36816
- const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
37068
+ const data = await cachedRequest(getHttpCache(), `/repos/${owner}/${repo}/contents/${path5}`, (headers) => octokit.repos.getContent({ owner, repo, path: path5, headers }));
36817
37069
  if (data && typeof data === "object" && "content" in data && typeof data.content === "string") {
36818
37070
  return {
36819
37071
  text: Buffer.from(data.content, "base64").toString("utf-8"),
@@ -36833,6 +37085,30 @@ async function probeRepoFile(octokit, owner, repo, path5) {
36833
37085
 
36834
37086
  // ../core/dist/core/repo-health.js
36835
37087
  var MODULE7 = "repo-health";
37088
+ var MERGE_WINDOW_DAYS = 90;
37089
+ async function fetchRepoMergeStats(octokit, owner, repo, tracker) {
37090
+ const since = new Date(Date.now() - MERGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
37091
+ const count = async (qualifiers) => {
37092
+ await tracker.waitForBudget();
37093
+ try {
37094
+ const { data } = await octokit.search.issuesAndPullRequests({
37095
+ q: `repo:${owner}/${repo} ${qualifiers} closed:>=${since}`,
37096
+ per_page: 1
37097
+ // only total_count is needed
37098
+ });
37099
+ return data.total_count;
37100
+ } finally {
37101
+ tracker.recordCall();
37102
+ }
37103
+ };
37104
+ const merged = await count("is:pr is:merged");
37105
+ const closedUnmerged = await count("is:pr is:unmerged is:closed");
37106
+ const denom = merged + closedUnmerged;
37107
+ return {
37108
+ recentMergedPRCount: merged,
37109
+ recentMergeRate: denom > 0 ? merged / denom : null
37110
+ };
37111
+ }
36836
37112
  var guidelinesCache = /* @__PURE__ */ new Map();
36837
37113
  var CACHE_TTL_MS = 60 * 60 * 1e3;
36838
37114
  var HEALTH_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
@@ -36852,22 +37128,25 @@ function pruneCache() {
36852
37128
  }
36853
37129
  }
36854
37130
  }
36855
- async function checkProjectHealth(octokit, owner, repo) {
37131
+ async function checkProjectHealth(octokit, owner, repo, tracker = getSearchBudgetTracker()) {
36856
37132
  const cache = getHttpCache();
36857
- const healthCacheKey = `health:${owner}/${repo}`;
37133
+ const healthCacheKey = versionedCacheKey(`health:${owner}/${repo}`);
36858
37134
  try {
36859
37135
  return await cachedTimeBased(cache, healthCacheKey, HEALTH_CACHE_TTL_MS, async () => {
36860
37136
  const url2 = `/repos/${owner}/${repo}`;
36861
37137
  const repoData = await cachedRequest(cache, url2, (headers) => octokit.repos.get({ owner, repo, headers }));
36862
- const { data: commits } = await octokit.repos.listCommits({
37138
+ const commitsUrl = `/repos/${owner}/${repo}/commits?per_page=1`;
37139
+ const commits = await cachedRequest(cache, commitsUrl, (headers) => octokit.repos.listCommits({
36863
37140
  owner,
36864
37141
  repo,
36865
- per_page: 1
36866
- });
37142
+ per_page: 1,
37143
+ headers
37144
+ }));
36867
37145
  const lastCommit = commits[0];
36868
37146
  const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
36869
37147
  const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
36870
37148
  const ciStatus = "unknown";
37149
+ const mergeStats = await fetchRepoMergeStats(octokit, owner, repo, tracker);
36871
37150
  return {
36872
37151
  repo: `${owner}/${repo}`,
36873
37152
  lastCommitAt,
@@ -36879,7 +37158,9 @@ async function checkProjectHealth(octokit, owner, repo) {
36879
37158
  isActive: daysSinceLastCommit < 30,
36880
37159
  stargazersCount: repoData.stargazers_count,
36881
37160
  forksCount: repoData.forks_count,
36882
- language: repoData.language
37161
+ language: repoData.language,
37162
+ recentMergedPRCount: mergeStats.recentMergedPRCount,
37163
+ recentMergeRate: mergeStats.recentMergeRate
36883
37164
  };
36884
37165
  });
36885
37166
  } catch (error48) {
@@ -37184,8 +37465,133 @@ ${selections.join("\n")}
37184
37465
  });
37185
37466
  return result;
37186
37467
  }
37468
+ var GH_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
37469
+ var MERGED_PR_BATCH_SIZE = 15;
37470
+ async function prefetchMergedPRCounts(octokit, username, repos) {
37471
+ if (repos.length === 0)
37472
+ return;
37473
+ if (!GH_NAME_PATTERN.test(username)) {
37474
+ warn(MODULE8, `Skipping merged-PR prefetch: username failed validation (falling back to REST)`);
37475
+ return;
37476
+ }
37477
+ const seen = /* @__PURE__ */ new Set();
37478
+ const valid = [];
37479
+ for (const r of repos) {
37480
+ const full = `${r.owner}/${r.repo}`;
37481
+ if (seen.has(full))
37482
+ continue;
37483
+ seen.add(full);
37484
+ if (!GH_NAME_PATTERN.test(r.owner) || !GH_NAME_PATTERN.test(r.repo)) {
37485
+ warn(MODULE8, `Skipping merged-PR prefetch for "${full}": failed validation (falling back to REST)`);
37486
+ continue;
37487
+ }
37488
+ valid.push(r);
37489
+ }
37490
+ if (valid.length === 0)
37491
+ return;
37492
+ const cache = getHttpCache();
37493
+ const batches = [];
37494
+ for (let i = 0; i < valid.length; i += MERGED_PR_BATCH_SIZE) {
37495
+ batches.push(fetchMergedPRBatch(octokit, username, valid.slice(i, i + MERGED_PR_BATCH_SIZE), cache));
37496
+ }
37497
+ await Promise.all(batches);
37498
+ }
37499
+ async function fetchMergedPRBatch(octokit, username, repos, cache) {
37500
+ const varDefs = [];
37501
+ const selections = ["viewer { login }"];
37502
+ const variables = {};
37503
+ repos.forEach((r, i) => {
37504
+ varDefs.push(`$q${i}: String!`);
37505
+ variables[`q${i}`] = `is:pr is:merged author:${username} repo:${r.owner}/${r.repo}`;
37506
+ selections.push(`r${i}: search(query: $q${i}, type: ISSUE, first: 1) { issueCount }`);
37507
+ });
37508
+ const query = `query batchMergedPRCounts(${varDefs.join(", ")}) {
37509
+ ${selections.join("\n")}
37510
+ }`;
37511
+ let data;
37512
+ try {
37513
+ data = await octokit.graphql(query, variables);
37514
+ } catch (err) {
37515
+ rethrowIfFatal(err);
37516
+ const partial2 = err.data;
37517
+ if (partial2) {
37518
+ data = partial2;
37519
+ } else {
37520
+ warn(MODULE8, `Merged-PR prefetch batch failed, falling back to REST: ${errorMessage(err)}`);
37521
+ return;
37522
+ }
37523
+ }
37524
+ const viewerLogin = data?.viewer?.login;
37525
+ if (!viewerLogin || viewerLogin.toLowerCase() !== username.toLowerCase()) {
37526
+ if (viewerLogin) {
37527
+ warn(MODULE8, `Merged-PR prefetch identity mismatch (configured username != authenticated login); leaving cache cold so REST @me answers`);
37528
+ }
37529
+ return;
37530
+ }
37531
+ repos.forEach((r, i) => {
37532
+ const alias = data?.[`r${i}`];
37533
+ const count = alias && "issueCount" in alias ? alias.issueCount : void 0;
37534
+ if (typeof count !== "number")
37535
+ return;
37536
+ cache.set(mergedPRsCacheKey(r.owner, r.repo), "", count);
37537
+ });
37538
+ }
37539
+ async function graphqlSearchIssues(octokit, query, first) {
37540
+ const clampedFirst = Math.min(Math.max(Math.trunc(first) || 1, 1), 100);
37541
+ const document = `query searchIssuesBroad($q: String!, $first: Int!) {
37542
+ search(query: $q, type: ISSUE, first: $first) {
37543
+ issueCount
37544
+ nodes {
37545
+ ... on Issue {
37546
+ url
37547
+ title
37548
+ updatedAt
37549
+ labels(first: 20) { nodes { name } }
37550
+ repository { nameWithOwner }
37551
+ }
37552
+ }
37553
+ }
37554
+ }`;
37555
+ let data;
37556
+ try {
37557
+ data = await octokit.graphql(document, {
37558
+ q: query,
37559
+ first: clampedFirst
37560
+ });
37561
+ } catch (err) {
37562
+ rethrowIfFatal(err);
37563
+ const partial2 = err.data;
37564
+ if (partial2) {
37565
+ data = partial2;
37566
+ } else {
37567
+ warn(MODULE8, `GraphQL broad search failed, falling back to REST: ${errorMessage(err)}`);
37568
+ return null;
37569
+ }
37570
+ }
37571
+ const search = data?.search;
37572
+ if (!search)
37573
+ return null;
37574
+ const items = [];
37575
+ for (const node of search.nodes ?? []) {
37576
+ if (!node?.url)
37577
+ continue;
37578
+ const nameWithOwner = node.repository?.nameWithOwner;
37579
+ if (!nameWithOwner)
37580
+ continue;
37581
+ items.push({
37582
+ html_url: node.url,
37583
+ // Same construction as the REST search adapter (search-phases.ts).
37584
+ repository_url: `https://api.github.com/repos/${nameWithOwner}`,
37585
+ updated_at: node.updatedAt ?? "",
37586
+ title: node.title,
37587
+ labels: node.labels?.nodes ?? []
37588
+ });
37589
+ }
37590
+ return { total_count: search.issueCount ?? 0, items };
37591
+ }
37187
37592
 
37188
37593
  // ../core/dist/core/slm-triage.js
37594
+ var MODULE9 = "slm-triage";
37189
37595
  var DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
37190
37596
  var DEFAULT_TIMEOUT_MS = 15e3;
37191
37597
  var MAX_BODY_CHARS = 2e3;
@@ -37222,10 +37628,48 @@ function buildPrompt(input) {
37222
37628
  `Linked PR exists: ${linkedPRExists}`
37223
37629
  ].join("\n");
37224
37630
  }
37631
+ function isAllowedTriageHost(raw) {
37632
+ let url2;
37633
+ try {
37634
+ url2 = new URL(raw);
37635
+ } catch {
37636
+ return false;
37637
+ }
37638
+ if (url2.protocol !== "http:" && url2.protocol !== "https:")
37639
+ return false;
37640
+ const host = url2.hostname.replace(/^\[|\]$/g, "").toLowerCase();
37641
+ if (host === "localhost" || host === "::1")
37642
+ return true;
37643
+ const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
37644
+ if (v4) {
37645
+ const a = Number(v4[1]);
37646
+ const b = Number(v4[2]);
37647
+ if (a === 127 || a === 10)
37648
+ return true;
37649
+ if (a === 192 && b === 168)
37650
+ return true;
37651
+ if (a === 172 && b >= 16 && b <= 31)
37652
+ return true;
37653
+ return false;
37654
+ }
37655
+ if (host.includes(":")) {
37656
+ return host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80");
37657
+ }
37658
+ return !host.includes(".") || host.endsWith(".local");
37659
+ }
37660
+ function assertValidTriageHost(value) {
37661
+ if (value === "" || isAllowedTriageHost(value))
37662
+ return;
37663
+ throw new ValidationError(`slmTriageHost must be an http(s) URL pointing at localhost or a private/LAN address (got "${value}"). Example: http://192.168.1.20:11434`);
37664
+ }
37225
37665
  async function triageWithSLM(input, options) {
37226
37666
  if (!options.model)
37227
37667
  return null;
37228
37668
  const host = options.host ?? DEFAULT_OLLAMA_HOST;
37669
+ if (!isAllowedTriageHost(host)) {
37670
+ warn(MODULE9, `slmTriageHost "${host}" is not a local/private address; skipping SLM triage`);
37671
+ return null;
37672
+ }
37229
37673
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
37230
37674
  const fetchFn = options.fetchImpl ?? fetch;
37231
37675
  let response;
@@ -37240,7 +37684,11 @@ async function triageWithSLM(input, options) {
37240
37684
  format: SLM_TRIAGE_SCHEMA,
37241
37685
  options: { temperature: 0.1 }
37242
37686
  }),
37243
- signal: AbortSignal.timeout(timeoutMs)
37687
+ signal: AbortSignal.timeout(timeoutMs),
37688
+ // No redirect following: an allowed private host could otherwise
37689
+ // 307/308-redirect the issue-text POST body to a public origin (#310).
37690
+ // Ollama never redirects, so this costs nothing.
37691
+ redirect: "error"
37244
37692
  });
37245
37693
  } catch {
37246
37694
  return null;
@@ -37297,15 +37745,23 @@ function isValidTriageShape(value) {
37297
37745
  }
37298
37746
 
37299
37747
  // ../core/dist/core/issue-vetting.js
37300
- var MODULE9 = "issue-vetting";
37748
+ var MODULE10 = "issue-vetting";
37301
37749
  var MAX_CONCURRENT_VETTING = 3;
37302
37750
  var VETTING_CACHE_TTL_MS = 15 * 60 * 1e3;
37751
+ function repoAcceptsContributionsFromHealth(projectHealth) {
37752
+ if (projectHealth.checkFailed || projectHealth.recentMergedPRCount == null) {
37753
+ return null;
37754
+ }
37755
+ if (projectHealth.recentMergedPRCount > 0)
37756
+ return true;
37757
+ return projectHealth.recentMergeRate == null ? null : false;
37758
+ }
37303
37759
  function deriveRecommendation(input) {
37304
37760
  const notes = [];
37305
37761
  const reasonsToApprove = [];
37306
37762
  const reasonsToSkip = [];
37307
37763
  if (!input.noExistingPR)
37308
- notes.push(input.ownPR ? "Your PR is already in flight for this issue" : "Existing PR found for this issue");
37764
+ notes.push(input.ownPR ? "Your PR is already in flight for this issue" : input.linkedPRMerged ? "A PR for this issue was already merged" : input.linkedPRClosed ? "A PR for this issue was closed without merging" : "Existing PR found for this issue");
37309
37765
  if (!input.notClaimed)
37310
37766
  notes.push("Issue appears to be claimed by someone");
37311
37767
  if (input.existingPRInconclusive) {
@@ -37323,14 +37779,28 @@ function deriveRecommendation(input) {
37323
37779
  notes.push("Issue requirements are unclear");
37324
37780
  if (!input.contributionGuidelinesFound)
37325
37781
  notes.push("No CONTRIBUTING.md found");
37326
- if (!input.noExistingPR)
37327
- reasonsToSkip.push(input.ownPR ? "You already have a PR in flight" : "Has existing PR");
37782
+ if (input.repoAcceptsContributions === false)
37783
+ notes.push("Repo has merged no PRs in the last 90 days");
37784
+ else if (input.repoAcceptsContributions === null)
37785
+ notes.push("Could not verify whether the repo merges PRs");
37786
+ if (!input.noExistingPR) {
37787
+ if (input.ownPR)
37788
+ reasonsToSkip.push("You already have a PR in flight");
37789
+ else if (input.linkedPRMerged)
37790
+ reasonsToSkip.push("Linked PR already merged");
37791
+ else if (input.linkedPRClosed)
37792
+ reasonsToSkip.push("Linked PR closed without merge");
37793
+ else
37794
+ reasonsToSkip.push("Has existing PR");
37795
+ }
37328
37796
  if (!input.notClaimed)
37329
37797
  reasonsToSkip.push("Already claimed");
37330
37798
  if (!input.projectIsActive && !input.projectCheckFailed)
37331
37799
  reasonsToSkip.push("Inactive project");
37332
37800
  if (!input.clearRequirements)
37333
37801
  reasonsToSkip.push("Unclear requirements");
37802
+ if (input.repoAcceptsContributions === false)
37803
+ reasonsToSkip.push("Repo has no recent merged PRs");
37334
37804
  if (input.noExistingPR)
37335
37805
  reasonsToApprove.push("No existing PR");
37336
37806
  if (input.notClaimed)
@@ -37356,16 +37826,18 @@ function deriveRecommendation(input) {
37356
37826
  let recommendation;
37357
37827
  if (input.issueClosed) {
37358
37828
  recommendation = "skip";
37829
+ } else if (input.linkedPRMerged || input.linkedPRClosed) {
37830
+ recommendation = "skip";
37359
37831
  } else if (input.ownPR) {
37360
37832
  recommendation = "skip";
37361
- } else if (input.passedAllChecks) {
37833
+ } else if (input.passedAllChecks && input.repoAcceptsContributions !== false) {
37362
37834
  recommendation = "approve";
37363
37835
  } else if (reasonsToSkip.length > 2) {
37364
37836
  recommendation = "skip";
37365
37837
  } else {
37366
37838
  recommendation = "needs_review";
37367
37839
  }
37368
- const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive;
37840
+ const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive || input.repoAcceptsContributions === null;
37369
37841
  if (recommendation === "approve" && hasInconclusiveChecks) {
37370
37842
  recommendation = "needs_review";
37371
37843
  notes.push("Recommendation downgraded: one or more checks were inconclusive");
@@ -37403,7 +37875,7 @@ var IssueVetter = class {
37403
37875
  const cacheKey = versionedCacheKey(`vet:${issueUrl}${sigKey}`);
37404
37876
  const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
37405
37877
  if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
37406
- debug(MODULE9, `Vetting cache hit for ${issueUrl}`);
37878
+ debug(MODULE10, `Vetting cache hit for ${issueUrl}`);
37407
37879
  return cached2;
37408
37880
  }
37409
37881
  const parsed = parseGitHubUrl(issueUrl);
@@ -37418,7 +37890,7 @@ var IssueVetter = class {
37418
37890
  const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
37419
37891
  checkNoExistingPR(this.octokit, owner, repo, number4),
37420
37892
  checkNotClaimed(this.octokit, owner, repo, number4, core.commentCount),
37421
- checkProjectHealth(this.octokit, owner, repo),
37893
+ checkProjectHealth(this.octokit, owner, repo, this.budgetTracker),
37422
37894
  fetchContributionGuidelines(this.octokit, owner, repo),
37423
37895
  hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo, this.budgetTracker)
37424
37896
  ]);
@@ -37428,8 +37900,11 @@ var IssueVetter = class {
37428
37900
  const username = this.stateReader.getGitHubUsername?.() ?? "";
37429
37901
  const linkedPR = existingPRCheck.linkedPR;
37430
37902
  const ownPR = !noExistingPR && !!linkedPR && linkedPR.state === "open" && !linkedPR.merged && username !== "" && linkedPR.author.toLowerCase() === username.toLowerCase();
37903
+ const linkedPRMerged = !!linkedPR && linkedPR.merged;
37904
+ const linkedPRClosed = !!linkedPR && linkedPR.state === "closed" && !linkedPR.merged;
37431
37905
  const clearRequirements = analyzeRequirements(core.body);
37432
37906
  const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
37907
+ const repoAcceptsContributions = repoAcceptsContributionsFromHealth(projectHealth);
37433
37908
  const vettingResult = {
37434
37909
  passedAllChecks: noExistingPR && notClaimed && projectActive && clearRequirements,
37435
37910
  checks: {
@@ -37465,10 +37940,12 @@ var IssueVetter = class {
37465
37940
  }
37466
37941
  const matchesCategory = repoBelongsToCategory(repoFullName, this.stateReader.getProjectCategories());
37467
37942
  const issueClosed = core.state === "closed";
37468
- const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive;
37943
+ const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive || repoAcceptsContributions === null;
37469
37944
  const { notes, reasonsToApprove, reasonsToSkip, recommendation } = deriveRecommendation({
37470
37945
  noExistingPR,
37471
37946
  ownPR,
37947
+ linkedPRMerged,
37948
+ linkedPRClosed,
37472
37949
  notClaimed,
37473
37950
  clearRequirements,
37474
37951
  contributionGuidelinesFound: !!contributionGuidelines,
@@ -37485,7 +37962,8 @@ var IssueVetter = class {
37485
37962
  orgHasMergedPRs,
37486
37963
  matchesCategory,
37487
37964
  issueClosed,
37488
- passedAllChecks: vettingResult.passedAllChecks
37965
+ passedAllChecks: vettingResult.passedAllChecks,
37966
+ repoAcceptsContributions
37489
37967
  });
37490
37968
  vettingResult.notes = notes;
37491
37969
  const repoQualityBonus = projectHealth.checkFailed ? 0 : calculateRepoQualityBonus(projectHealth.stargazersCount ?? 0, projectHealth.forksCount ?? 0);
@@ -37565,6 +38043,36 @@ var IssueVetter = class {
37565
38043
  updatedAt: ghIssue.updated_at
37566
38044
  };
37567
38045
  }
38046
+ /**
38047
+ * Batch-prefetch merged-PR counts for the repos in a vetting batch (#182),
38048
+ * warming the cache `checkUserMergedPRsInRepo` reads so each per-repo Search
38049
+ * API call becomes a cache hit.
38050
+ *
38051
+ * Two repos are intentionally excluded:
38052
+ * - Repos already known to have the user's merged PRs
38053
+ * (`getReposWithMergedPRs`): `vetIssue` short-circuits those to a merged
38054
+ * count of 1 and never calls the Search API, so prefetching them is waste.
38055
+ * - Everything, when the GitHub username is unknown: the GraphQL `author:`
38056
+ * filter needs a concrete login and the REST path falls back to `@me`.
38057
+ */
38058
+ async prefetchMergedPRCounts(issueRefs) {
38059
+ const username = this.stateReader.getGitHubUsername?.() ?? "";
38060
+ if (!username)
38061
+ return;
38062
+ const alreadyKnown = new Set(this.stateReader.getReposWithMergedPRs());
38063
+ const seen = /* @__PURE__ */ new Set();
38064
+ const repos = [];
38065
+ for (const { owner, repo } of issueRefs) {
38066
+ const full = `${owner}/${repo}`;
38067
+ if (alreadyKnown.has(full) || seen.has(full))
38068
+ continue;
38069
+ seen.add(full);
38070
+ repos.push({ owner, repo });
38071
+ }
38072
+ if (repos.length === 0)
38073
+ return;
38074
+ await prefetchMergedPRCounts(this.octokit, username, repos);
38075
+ }
37568
38076
  /**
37569
38077
  * Vet multiple issues in parallel with concurrency limit
37570
38078
  */
@@ -37575,11 +38083,18 @@ var IssueVetter = class {
37575
38083
  let rateLimitFailures = 0;
37576
38084
  let attemptedCount = 0;
37577
38085
  let firstAuthError = null;
38086
+ let firstForbiddenError = null;
38087
+ let forbiddenCount = 0;
38088
+ const forbiddenRepos = /* @__PURE__ */ new Set();
37578
38089
  const uniqueUrls = [...new Set(urls)];
37579
- const prefetched = await prefetchIssueCores(this.octokit, uniqueUrls.flatMap((url2) => {
38090
+ const issueRefs = uniqueUrls.flatMap((url2) => {
37580
38091
  const p = parseGitHubUrl(url2);
37581
38092
  return p && p.type === "issues" ? [{ owner: p.owner, repo: p.repo, number: p.number }] : [];
37582
- }));
38093
+ });
38094
+ const [prefetched] = await Promise.all([
38095
+ prefetchIssueCores(this.octokit, issueRefs),
38096
+ this.prefetchMergedPRCounts(issueRefs)
38097
+ ]);
37583
38098
  const prefetchFor = (url2) => {
37584
38099
  const p = parseGitHubUrl(url2);
37585
38100
  return p && p.type === "issues" ? prefetched.get(issueCoreKey(p.owner, p.repo, p.number)) : void 0;
@@ -37589,6 +38104,12 @@ var IssueVetter = class {
37589
38104
  break;
37590
38105
  if (firstAuthError)
37591
38106
  break;
38107
+ const parsedUrl = parseGitHubUrl(url2);
38108
+ const repoKey = parsedUrl ? `${parsedUrl.owner}/${parsedUrl.repo}` : null;
38109
+ if (repoKey && forbiddenRepos.has(repoKey)) {
38110
+ debug(MODULE10, `Skipping ${url2}: repo already returned 403`);
38111
+ continue;
38112
+ }
37592
38113
  attemptedCount++;
37593
38114
  const core = prefetchFor(url2);
37594
38115
  const task = this.vetIssue(url2, core ? { prefetched: core } : void 0).then((candidate) => {
@@ -37603,11 +38124,20 @@ var IssueVetter = class {
37603
38124
  firstAuthError ??= error48;
37604
38125
  return;
37605
38126
  }
38127
+ if (isAuthError(error48)) {
38128
+ firstForbiddenError ??= error48;
38129
+ forbiddenCount++;
38130
+ failedVettingCount++;
38131
+ if (repoKey)
38132
+ forbiddenRepos.add(repoKey);
38133
+ warn(MODULE10, `Access forbidden for ${url2} (SAML/token scope?) \u2014 skipping this repo:`, errorMessage(error48));
38134
+ return;
38135
+ }
37606
38136
  failedVettingCount++;
37607
38137
  if (isRateLimitError(error48)) {
37608
38138
  rateLimitFailures++;
37609
38139
  }
37610
- warn(MODULE9, `Error vetting issue ${url2}:`, errorMessage(error48));
38140
+ warn(MODULE10, `Error vetting issue ${url2}:`, errorMessage(error48));
37611
38141
  }).finally(() => pending.delete(url2));
37612
38142
  pending.set(url2, task);
37613
38143
  if (pending.size >= MAX_CONCURRENT_VETTING) {
@@ -37617,13 +38147,16 @@ var IssueVetter = class {
37617
38147
  await Promise.allSettled(pending.values());
37618
38148
  if (firstAuthError) {
37619
38149
  if (candidates.length > 0) {
37620
- warn(MODULE9, `Auth failed mid-batch after ${candidates.length} successful vet(s) \u2014 discarding partial results`);
38150
+ warn(MODULE10, `Auth failed mid-batch after ${candidates.length} successful vet(s) \u2014 discarding partial results`);
37621
38151
  }
37622
38152
  throw firstAuthError;
37623
38153
  }
38154
+ if (firstForbiddenError && attemptedCount > 0 && forbiddenCount === attemptedCount) {
38155
+ throw firstForbiddenError;
38156
+ }
37624
38157
  const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
37625
38158
  if (allFailed) {
37626
- warn(MODULE9, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
38159
+ warn(MODULE10, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
37627
38160
  }
37628
38161
  return {
37629
38162
  candidates: candidates.slice(0, maxResults),
@@ -37634,14 +38167,22 @@ var IssueVetter = class {
37634
38167
  };
37635
38168
 
37636
38169
  // ../core/dist/core/search-phases.js
37637
- var MODULE10 = "search-phases";
38170
+ var MODULE11 = "search-phases";
37638
38171
  var GITHUB_MAX_BOOLEAN_OPS = 5;
37639
38172
  var INTER_QUERY_DELAY_MS = 2e3;
38173
+ var GRAPHQL_INTER_QUERY_DELAY_MS = 500;
38174
+ var graphqlSearchQueryCount = 0;
38175
+ function getGraphQLSearchQueryCount() {
38176
+ return graphqlSearchQueryCount;
38177
+ }
38178
+ function resetGraphQLSearchQueryCount() {
38179
+ graphqlSearchQueryCount = 0;
38180
+ }
37640
38181
  function chunkLabels(labels, reservedOps = 0) {
37641
38182
  const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
37642
38183
  if (maxPerChunk < 1) {
37643
38184
  if (labels.length > 0) {
37644
- warn(MODULE10, `Label filtering disabled: ${reservedOps} repo/org ORs exceed GitHub's ${GITHUB_MAX_BOOLEAN_OPS} operator limit. All ${labels.length} label(s) dropped from query.`);
38185
+ warn(MODULE11, `Label filtering disabled: ${reservedOps} repo/org ORs exceed GitHub's ${GITHUB_MAX_BOOLEAN_OPS} operator limit. All ${labels.length} label(s) dropped from query.`);
37645
38186
  }
37646
38187
  return [[]];
37647
38188
  }
@@ -37651,7 +38192,7 @@ function chunkLabels(labels, reservedOps = 0) {
37651
38192
  for (let i = 0; i < labels.length; i += maxPerChunk) {
37652
38193
  chunks.push(labels.slice(i, i + maxPerChunk));
37653
38194
  }
37654
- debug(MODULE10, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
38195
+ debug(MODULE11, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
37655
38196
  return chunks;
37656
38197
  }
37657
38198
  function buildLabelQuery(labels) {
@@ -37688,7 +38229,7 @@ async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTrac
37688
38229
  const cache = getHttpCache();
37689
38230
  const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
37690
38231
  if (cached2) {
37691
- debug(MODULE10, `Search cache hit for query`);
38232
+ debug(MODULE11, `Search cache hit for query`);
37692
38233
  return cached2;
37693
38234
  }
37694
38235
  await tracker.waitForBudget();
@@ -37702,10 +38243,32 @@ async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTrac
37702
38243
  if (data.items.length > 0) {
37703
38244
  cache.set(cacheKey, "", data);
37704
38245
  } else {
37705
- debug(MODULE10, `Skipping cache for empty search result (possible rate limit artifact)`);
38246
+ debug(MODULE11, `Skipping cache for empty search result (possible rate limit artifact)`);
37706
38247
  }
37707
38248
  return data;
37708
38249
  }
38250
+ async function searchIssuesGraphQLFirst(octokit, params, tracker = getSearchBudgetTracker()) {
38251
+ const cacheKey = versionedCacheKey(`gqlsearch:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
38252
+ const cache = getHttpCache();
38253
+ const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
38254
+ if (cached2) {
38255
+ debug(MODULE11, `GraphQL search cache hit for query`);
38256
+ return cached2;
38257
+ }
38258
+ const gqlQuery = `${params.q} sort:${params.sort}-${params.order}`;
38259
+ const gql = await graphqlSearchIssues(octokit, gqlQuery, Math.min(params.per_page, 100));
38260
+ if (gql) {
38261
+ graphqlSearchQueryCount++;
38262
+ if (gql.items.length > 0) {
38263
+ cache.set(cacheKey, "", gql);
38264
+ } else {
38265
+ debug(MODULE11, `Skipping cache for empty GraphQL search result (possible transient artifact)`);
38266
+ }
38267
+ return gql;
38268
+ }
38269
+ warn(MODULE11, `GraphQL broad search unavailable; falling back to REST search API`);
38270
+ return cachedSearchIssues(octokit, params, tracker);
38271
+ }
37709
38272
  async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResults) {
37710
38273
  const items = [];
37711
38274
  for (const repoFullName of repos) {
@@ -37750,15 +38313,15 @@ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResul
37750
38313
  if (getHttpStatusCode(error48) === 401)
37751
38314
  throw error48;
37752
38315
  if (isRateLimitError(error48)) {
37753
- warn(MODULE10, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
38316
+ warn(MODULE11, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
37754
38317
  break;
37755
38318
  }
37756
- warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
38319
+ warn(MODULE11, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
37757
38320
  }
37758
38321
  }
37759
38322
  return items;
37760
38323
  }
37761
- async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn) {
38324
+ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn, perPage = 5) {
37762
38325
  const candidates = [];
37763
38326
  let failedRepos = 0;
37764
38327
  let rateLimitFailures = 0;
@@ -37780,7 +38343,7 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
37780
38343
  state: "open",
37781
38344
  sort: "created",
37782
38345
  direction: "desc",
37783
- per_page: 5,
38346
+ per_page: perPage,
37784
38347
  ...label !== void 0 ? { labels: label } : {}
37785
38348
  });
37786
38349
  for (const issue2 of response.data) {
@@ -37815,13 +38378,13 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
37815
38378
  if (isRateLimitError(error48)) {
37816
38379
  rateLimitFailures++;
37817
38380
  }
37818
- warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
38381
+ warn(MODULE11, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
37819
38382
  }
37820
38383
  }
37821
38384
  const allReposFailed = failedRepos === repos.length && repos.length > 0;
37822
38385
  const rateLimitHit = rateLimitFailures > 0;
37823
38386
  if (allReposFailed) {
37824
- warn(MODULE10, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
38387
+ warn(MODULE11, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
37825
38388
  }
37826
38389
  return { candidates, allReposFailed, rateLimitHit };
37827
38390
  }
@@ -37831,9 +38394,9 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
37831
38394
  const allItems = [];
37832
38395
  for (let i = 0; i < labelChunks.length; i++) {
37833
38396
  if (i > 0)
37834
- await sleep(INTER_QUERY_DELAY_MS);
38397
+ await sleep(GRAPHQL_INTER_QUERY_DELAY_MS);
37835
38398
  const query = buildQuery(buildLabelQuery(labelChunks[i]));
37836
- const data = await cachedSearchIssues(octokit, {
38399
+ const data = await searchIssuesGraphQLFirst(octokit, {
37837
38400
  q: query,
37838
38401
  sort: "created",
37839
38402
  order: "desc",
@@ -37857,13 +38420,15 @@ function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
37857
38420
  return [languages.map((l) => `language:${l}`).join(" ")];
37858
38421
  return languages.map((l) => `language:${l}`);
37859
38422
  }
37860
- async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker()) {
37861
- const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
38423
+ async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker(), startOffset = 0) {
38424
+ const variants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
38425
+ const k = variants.length > 0 ? startOffset % variants.length : 0;
38426
+ const langVariants = [...variants.slice(k), ...variants.slice(0, k)];
37862
38427
  const seenUrls = /* @__PURE__ */ new Set();
37863
38428
  const allItems = [];
37864
38429
  for (let i = 0; i < langVariants.length; i++) {
37865
38430
  if (i > 0)
37866
- await sleep(INTER_QUERY_DELAY_MS);
38431
+ await sleep(GRAPHQL_INTER_QUERY_DELAY_MS);
37867
38432
  const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage, tracker);
37868
38433
  for (const item of items) {
37869
38434
  if (!seenUrls.has(item.html_url)) {
@@ -37878,7 +38443,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37878
38443
  const spamRepos = detectLabelFarmingRepos(items);
37879
38444
  if (spamRepos.size > 0) {
37880
38445
  const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
37881
- debug(MODULE10, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
38446
+ debug(MODULE11, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
37882
38447
  }
37883
38448
  const itemsToVet = filterIssues(items).filter((item) => {
37884
38449
  const repoFullName = extractRepoFromUrl(item.repository_url);
@@ -37889,7 +38454,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37889
38454
  return excludedRepoSets.every((s) => !s.has(repoFullName));
37890
38455
  }).slice(0, remainingNeeded * 2);
37891
38456
  if (itemsToVet.length === 0) {
37892
- debug(MODULE10, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
38457
+ debug(MODULE11, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
37893
38458
  return { candidates: [], allVetFailed: false, rateLimitHit: false };
37894
38459
  }
37895
38460
  const { candidates: results, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
@@ -37901,7 +38466,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37901
38466
  });
37902
38467
  const starFilteredCount = results.length - starFiltered.length;
37903
38468
  if (starFilteredCount > 0) {
37904
- debug(MODULE10, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
38469
+ debug(MODULE11, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
37905
38470
  }
37906
38471
  return { candidates: starFiltered, allVetFailed, rateLimitHit };
37907
38472
  }
@@ -37994,9 +38559,12 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
37994
38559
  }
37995
38560
 
37996
38561
  // ../core/dist/core/issue-discovery.js
37997
- var MODULE11 = "issue-discovery";
38562
+ var MODULE12 = "issue-discovery";
37998
38563
  var LOW_BUDGET_THRESHOLD = 20;
37999
38564
  var CRITICAL_BUDGET_THRESHOLD = 10;
38565
+ var PHASE0_PER_PAGE = 30;
38566
+ var CONTRIBUTED_REPO_MAX_AGE_DAYS = 365;
38567
+ var PHASE0_MAX_SHARE = 0.5;
38000
38568
  function buildIssueFilter(config2) {
38001
38569
  return (items) => {
38002
38570
  return items.filter((item) => {
@@ -38018,6 +38586,8 @@ function buildIssueFilter(config2) {
38018
38586
  if (config2.skippedUrls.has(item.html_url))
38019
38587
  return false;
38020
38588
  const updatedAt = new Date(item.updated_at);
38589
+ if (Number.isNaN(updatedAt.getTime()))
38590
+ return false;
38021
38591
  const ageDays = daysBetween(updatedAt, config2.now);
38022
38592
  if (ageDays > config2.maxAgeDays)
38023
38593
  return false;
@@ -38028,9 +38598,9 @@ function buildIssueFilter(config2) {
38028
38598
  };
38029
38599
  }
38030
38600
  async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
38031
- info(MODULE11, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
38032
- const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
38033
- info(MODULE11, `Found ${candidates.length} candidates from merged-PR repos`);
38601
+ info(MODULE12, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter, ${PHASE0_PER_PAGE}/repo)...`);
38602
+ const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues, PHASE0_PER_PAGE);
38603
+ info(MODULE12, `Found ${candidates.length} candidates from merged-PR repos`);
38034
38604
  return {
38035
38605
  candidates,
38036
38606
  error: allReposFailed ? "All merged-PR repo fetches failed" : null,
@@ -38038,26 +38608,26 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
38038
38608
  };
38039
38609
  }
38040
38610
  async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
38041
- info(MODULE11, `Phase 1: Searching issues in ${repos.length} starred repos...`);
38611
+ info(MODULE12, `Phase 1: Searching issues in ${repos.length} starred repos...`);
38042
38612
  const phase1Labels = labels.slice(0, 3);
38043
38613
  const reposToSearch = repos.slice(0, 10);
38044
38614
  const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
38045
- info(MODULE11, `Found ${candidates.length} candidates from starred repos`);
38615
+ info(MODULE12, `Found ${candidates.length} candidates from starred repos`);
38046
38616
  return {
38047
38617
  candidates,
38048
38618
  error: allReposFailed ? "All starred repo fetches failed" : null,
38049
38619
  rateLimitHit
38050
38620
  };
38051
38621
  }
38052
- async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues, tracker) {
38053
- info(MODULE11, "Phase 2: General issue search...");
38622
+ async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues, tracker, languageRotationOffset = 0) {
38623
+ info(MODULE12, "Phase 2: General issue search...");
38054
38624
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38055
38625
  const tierLabelGroups = [];
38056
38626
  if (scopes && scopes.length > 1) {
38057
38627
  for (const scope of scopes) {
38058
38628
  const scopeLabels = SCOPE_LABELS[scope] ?? [];
38059
38629
  if (scopeLabels.length === 0) {
38060
- warn(MODULE11, `Scope "${scope}" has no labels, skipping tier`);
38630
+ warn(MODULE12, `Scope "${scope}" has no labels, skipping tier`);
38061
38631
  continue;
38062
38632
  }
38063
38633
  tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
@@ -38076,8 +38646,8 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38076
38646
  let rateLimitHit = false;
38077
38647
  for (const { tier, tierLabels } of tierLabelGroups) {
38078
38648
  try {
38079
- const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3, tracker);
38080
- info(MODULE11, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
38649
+ const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3, tracker, languageRotationOffset);
38650
+ info(MODULE12, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
38081
38651
  const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
38082
38652
  tierResults.push(tierCandidates);
38083
38653
  for (const c of tierCandidates)
@@ -38088,7 +38658,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38088
38658
  if (vetRateLimitHit) {
38089
38659
  rateLimitHit = true;
38090
38660
  }
38091
- info(MODULE11, `Found ${tierCandidates.length} candidates from ${tier} tier`);
38661
+ info(MODULE12, `Found ${tierCandidates.length} candidates from ${tier} tier`);
38092
38662
  } catch (err) {
38093
38663
  if (getHttpStatusCode(err) === 401)
38094
38664
  throw err;
@@ -38097,13 +38667,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38097
38667
  if (isRateLimitError(err)) {
38098
38668
  rateLimitHit = true;
38099
38669
  }
38100
- warn(MODULE11, `Error in ${tier} tier search: ${errMsg}`);
38670
+ warn(MODULE12, `Error in ${tier} tier search: ${errMsg}`);
38101
38671
  tierResults.push([]);
38102
38672
  }
38103
38673
  }
38104
38674
  const interleaved = interleaveArrays(tierResults);
38105
38675
  if (interleaved.length === 0 && error48) {
38106
- warn(MODULE11, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
38676
+ warn(MODULE12, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
38107
38677
  }
38108
38678
  return {
38109
38679
  candidates: interleaved.slice(0, maxResults),
@@ -38112,17 +38682,17 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38112
38682
  };
38113
38683
  }
38114
38684
  async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues, tracker) {
38115
- info(MODULE11, "Phase 3: Searching actively maintained repos...");
38685
+ info(MODULE12, "Phase 3: Searching actively maintained repos...");
38116
38686
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38117
38687
  const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
38118
38688
  if (eligibleStarred.length > 0) {
38119
- info(MODULE11, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
38689
+ info(MODULE12, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
38120
38690
  const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
38121
38691
  if (restItems.length > 0) {
38122
38692
  try {
38123
38693
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
38124
38694
  if (candidates.length > 0) {
38125
- info(MODULE11, `Found ${candidates.length} candidates from maintained-repo REST search`);
38695
+ info(MODULE12, `Found ${candidates.length} candidates from maintained-repo REST search`);
38126
38696
  return {
38127
38697
  candidates,
38128
38698
  error: allVetFailed ? "all vetting failed" : null,
@@ -38132,11 +38702,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38132
38702
  } catch (error48) {
38133
38703
  if (getHttpStatusCode(error48) === 401)
38134
38704
  throw error48;
38135
- warn(MODULE11, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
38705
+ warn(MODULE12, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
38136
38706
  }
38137
38707
  }
38138
38708
  }
38139
- info(MODULE11, "Phase 3: Falling back to Search API...");
38709
+ info(MODULE12, "Phase 3: Falling back to Search API...");
38140
38710
  const thirtyDaysAgo = /* @__PURE__ */ new Date();
38141
38711
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
38142
38712
  const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
@@ -38144,15 +38714,15 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38144
38714
  const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : "";
38145
38715
  const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
38146
38716
  try {
38147
- const data = await cachedSearchIssues(octokit, {
38717
+ const data = await searchIssuesGraphQLFirst(octokit, {
38148
38718
  q: phase3Query,
38149
38719
  sort: "updated",
38150
38720
  order: "desc",
38151
38721
  per_page: maxResults * 3
38152
38722
  }, tracker);
38153
- info(MODULE11, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
38723
+ info(MODULE12, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
38154
38724
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
38155
- info(MODULE11, `Found ${candidates.length} candidates from maintained-repo search`);
38725
+ info(MODULE12, `Found ${candidates.length} candidates from maintained-repo search`);
38156
38726
  return {
38157
38727
  candidates,
38158
38728
  error: allVetFailed ? "all vetting failed" : null,
@@ -38162,7 +38732,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38162
38732
  if (getHttpStatusCode(error48) === 401)
38163
38733
  throw error48;
38164
38734
  const errMsg = errorMessage(error48);
38165
- warn(MODULE11, `Error in maintained-repo search: ${errMsg}`);
38735
+ warn(MODULE12, `Error in maintained-repo search: ${errMsg}`);
38166
38736
  return {
38167
38737
  candidates: [],
38168
38738
  error: errMsg,
@@ -38246,7 +38816,7 @@ var IssueDiscovery = class {
38246
38816
  let rateLimitHitDuringSearch = false;
38247
38817
  const applyInterPhaseDelay = async () => {
38248
38818
  if (interPhaseDelay > 0) {
38249
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38819
+ info(MODULE12, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38250
38820
  await sleep(interPhaseDelay);
38251
38821
  }
38252
38822
  };
@@ -38256,9 +38826,12 @@ var IssueDiscovery = class {
38256
38826
  if (result.rateLimitHit)
38257
38827
  rateLimitHitDuringSearch = true;
38258
38828
  };
38829
+ const viableCandidateCount = () => allCandidates.filter((c) => c.recommendation !== "skip").length;
38259
38830
  this.rateLimitWarning = null;
38831
+ resetGraphQLSearchQueryCount();
38260
38832
  const tracker = this.budgetTracker;
38261
- let searchBudget = LOW_BUDGET_THRESHOLD - 1;
38833
+ let searchBudget = CRITICAL_BUDGET_THRESHOLD - 1;
38834
+ let preflightFailed = false;
38262
38835
  try {
38263
38836
  const rateLimit = await checkRateLimit(this.githubToken);
38264
38837
  searchBudget = rateLimit.remaining;
@@ -38266,22 +38839,23 @@ var IssueDiscovery = class {
38266
38839
  if (rateLimit.remaining < 5) {
38267
38840
  const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
38268
38841
  this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
38269
- warn(MODULE11, this.rateLimitWarning);
38842
+ warn(MODULE12, this.rateLimitWarning);
38270
38843
  }
38271
38844
  if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
38272
- info(MODULE11, `Search budget critical (${searchBudget} remaining) \u2014 running only Phase 0`);
38845
+ info(MODULE12, `Search budget critical (${searchBudget} remaining) \u2014 skipping the starred phase; broad/maintained still run on GraphQL (points bucket)`);
38273
38846
  } else if (searchBudget < LOW_BUDGET_THRESHOLD) {
38274
- info(MODULE11, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
38847
+ info(MODULE12, `Search budget low (${searchBudget} remaining) \u2014 broad/maintained run on GraphQL (points bucket), unaffected by REST search quota`);
38275
38848
  }
38276
38849
  } catch (error48) {
38277
38850
  if (getHttpStatusCode(error48) === 401)
38278
38851
  throw error48;
38852
+ preflightFailed = true;
38279
38853
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
38280
- warn(MODULE11, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
38854
+ warn(MODULE12, "Could not check rate limit \u2014 using conservative budget, skipping the starred phase:", errorMessage(error48));
38281
38855
  }
38282
38856
  if (searchBudget <= 0) {
38283
- this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
38284
- return { candidates: [], strategiesUsed: [] };
38857
+ this.rateLimitWarning = "GitHub search API quota exhausted \u2014 skipping the starred phase; other phases don't use it.";
38858
+ warn(MODULE12, this.rateLimitWarning);
38285
38859
  }
38286
38860
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
38287
38861
  const openPRRepos = this.stateReader.getReposWithOpenPRs();
@@ -38292,17 +38866,24 @@ var IssueDiscovery = class {
38292
38866
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
38293
38867
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist.map((r) => r.toLowerCase()));
38294
38868
  if (aiBlocklisted.size > 0) {
38295
- debug(MODULE11, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
38869
+ debug(MODULE12, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
38296
38870
  }
38297
- const filterIssues = buildIssueFilter({
38871
+ const baseFilterConfig = {
38298
38872
  excludedRepos: new Set(config2.excludeRepos.map((r) => r.toLowerCase())),
38299
38873
  excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
38300
38874
  aiBlocklisted,
38301
38875
  lowScoringRepos,
38302
38876
  skippedUrls: options.skippedUrls ?? /* @__PURE__ */ new Set(),
38303
- maxAgeDays: config2.maxIssueAgeDays || 90,
38304
38877
  now: /* @__PURE__ */ new Date(),
38305
38878
  includeDocIssues: config2.includeDocIssues ?? true
38879
+ };
38880
+ const filterIssues = buildIssueFilter({
38881
+ ...baseFilterConfig,
38882
+ maxAgeDays: config2.maxIssueAgeDays || 90
38883
+ });
38884
+ const filterIssuesPhase0 = buildIssueFilter({
38885
+ ...baseFilterConfig,
38886
+ maxAgeDays: CONTRIBUTED_REPO_MAX_AGE_DAYS
38306
38887
  });
38307
38888
  const seenPhase0 = /* @__PURE__ */ new Set();
38308
38889
  const phase0Repos = [];
@@ -38315,10 +38896,12 @@ var IssueDiscovery = class {
38315
38896
  break;
38316
38897
  }
38317
38898
  const phase0RepoSet = new Set(phase0Repos);
38899
+ const otherStrategiesCanRun = starredRepos.length > 0 && enabledStrategies.has("starred") || enabledStrategies.has("broad") || enabledStrategies.has("maintained");
38318
38900
  if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
38319
- const remaining = maxResults - allCandidates.length;
38901
+ const phase0Cap = otherStrategiesCanRun ? Math.max(1, Math.ceil(maxResults * PHASE0_MAX_SHARE)) : maxResults;
38902
+ const remaining = Math.min(maxResults - allCandidates.length, phase0Cap);
38320
38903
  if (remaining > 0) {
38321
- const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
38904
+ const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssuesPhase0);
38322
38905
  recordPhaseResult("0", result);
38323
38906
  }
38324
38907
  strategiesUsed.push("merged");
@@ -38338,37 +38921,39 @@ var IssueDiscovery = class {
38338
38921
  const broadDelay = options.broadPhaseDelayMs ?? config2.broadPhaseDelayMs ?? 9e4;
38339
38922
  const configuredSkipThreshold = config2.skipBroadWhenSufficientResults ?? 8;
38340
38923
  const skipThreshold = configuredSkipThreshold > 0 ? Math.min(configuredSkipThreshold, maxResults - 1) : 0;
38341
- if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
38342
- if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
38343
- info(MODULE11, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
38924
+ const viableBeforeBroad = viableCandidateCount();
38925
+ if (viableBeforeBroad >= maxResults && enabledStrategies.has("broad")) {
38926
+ info(MODULE12, `Skipping broad search: ${viableBeforeBroad} viable candidate(s) already meet maxResults (${maxResults})`);
38927
+ } else if (enabledStrategies.has("broad")) {
38928
+ const newRepoCandidateCount = allCandidates.filter((c) => c.recommendation !== "skip" && !phase0RepoSet.has(c.issue.repo) && !starredRepoSet.has(c.issue.repo)).length;
38929
+ if (skipThreshold > 0 && newRepoCandidateCount >= skipThreshold) {
38930
+ info(MODULE12, `Skipping broad search: already found ${newRepoCandidateCount} viable candidate(s) from new repos (threshold: ${skipThreshold})`);
38344
38931
  } else {
38345
38932
  await applyInterPhaseDelay();
38346
38933
  if (allCandidates.length > 0 && broadDelay > 0) {
38347
- info(MODULE11, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
38934
+ info(MODULE12, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
38348
38935
  await sleep(broadDelay);
38349
38936
  } else if (allCandidates.length === 0) {
38350
- info(MODULE11, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
38937
+ info(MODULE12, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
38351
38938
  }
38352
- const remaining = maxResults - allCandidates.length;
38353
- const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues, tracker);
38939
+ const remaining = maxResults - viableBeforeBroad;
38940
+ const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues, tracker, options.languageRotationOffset ?? 0);
38354
38941
  recordPhaseResult("2", result);
38355
38942
  strategiesUsed.push("broad");
38356
38943
  }
38357
38944
  }
38358
- if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
38945
+ const viableBeforeMaintained = viableCandidateCount();
38946
+ if (viableBeforeMaintained >= maxResults && enabledStrategies.has("maintained")) {
38947
+ info(MODULE12, `Skipping maintained search: ${viableBeforeMaintained} viable candidate(s) already meet maxResults (${maxResults})`);
38948
+ } else if (enabledStrategies.has("maintained")) {
38359
38949
  await applyInterPhaseDelay();
38360
- const remaining = maxResults - allCandidates.length;
38950
+ const remaining = maxResults - viableBeforeMaintained;
38361
38951
  const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues, tracker);
38362
38952
  recordPhaseResult("3", result);
38363
38953
  strategiesUsed.push("maintained");
38364
38954
  }
38365
- const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
38366
- let budgetNote = "";
38367
- if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
38368
- budgetNote = ` Most search phases were skipped due to critically low API quota (${searchBudget} remaining).`;
38369
- } else if (phasesSkippedForBudget) {
38370
- budgetNote = ` Some search phases were skipped due to low API quota (${searchBudget} remaining).`;
38371
- }
38955
+ const phasesSkippedForBudget = searchBudget < CRITICAL_BUDGET_THRESHOLD;
38956
+ const budgetNote = phasesSkippedForBudget ? preflightFailed ? " The starred-repo phase was skipped as a precaution because the rate-limit check failed (quota unknown); broad and maintained phases still ran on GraphQL." : ` The starred-repo phase was skipped due to critically low API quota (${searchBudget} remaining); broad and maintained phases still ran on GraphQL.` : "";
38372
38957
  if (allCandidates.length === 0) {
38373
38958
  const errorDetails = [
38374
38959
  phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
@@ -38381,7 +38966,7 @@ var IssueDiscovery = class {
38381
38966
  this.rateLimitWarning = `Search returned no results due to GitHub API rate limits.${details}${budgetNote} Try again after the rate limit resets.`;
38382
38967
  return { candidates: [], strategiesUsed };
38383
38968
  }
38384
- throw new ValidationError(`No issue candidates found across all search phases.${details} Try adjusting your search criteria (languages, labels) or check your network connection.`);
38969
+ throw new ValidationError(`No issue candidates found across all search phases.${details} Try adjusting your search criteria (languages, labels) or check your network connection.`, strategiesUsed);
38385
38970
  }
38386
38971
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
38387
38972
  this.rateLimitWarning = `Search results may be incomplete: GitHub API rate limits were hit during search.${budgetNote} Found ${allCandidates.length} candidate${allCandidates.length === 1 ? "" : "s"} but some search phases were limited. Try again after the rate limit resets for complete results.`;
@@ -38410,9 +38995,13 @@ var IssueDiscovery = class {
38410
38995
  return boostDiff;
38411
38996
  return b.viabilityScore - a.viabilityScore;
38412
38997
  });
38413
- const capped = applyPerRepoCap(ranked, 2);
38998
+ const viableFirst = [
38999
+ ...ranked.filter((c) => c.recommendation !== "skip"),
39000
+ ...ranked.filter((c) => c.recommendation === "skip")
39001
+ ];
39002
+ const capped = applyPerRepoCap(viableFirst, 2);
38414
39003
  const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
38415
- info(MODULE11, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${finalPicks.length} candidates returned`);
39004
+ info(MODULE12, `Search complete: ${tracker.getTotalCalls()} REST Search API call(s) + ${getGraphQLSearchQueryCount()} GraphQL broad-search query/queries used, ${finalPicks.length} candidates returned`);
38416
39005
  return { candidates: finalPicks, strategiesUsed };
38417
39006
  }
38418
39007
  /**
@@ -38464,18 +39053,25 @@ function pruneCache2() {
38464
39053
  function parseRoadmapIssueRefs(content, owner, repo) {
38465
39054
  const refs = /* @__PURE__ */ new Set();
38466
39055
  const ownerRepoLower = `${owner}/${repo}`.toLowerCase();
38467
- const escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38468
- const fullRefPattern = new RegExp(`\\b${escape2(owner)}/${escape2(repo)}#(\\d+)\\b`, "gi");
39056
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39057
+ const fullRefPattern = new RegExp(`\\b${escape(owner)}/${escape(repo)}#(\\d+)\\b`, "gi");
38469
39058
  for (const m of content.matchAll(fullRefPattern)) {
38470
39059
  const n = Number.parseInt(m[1], 10);
38471
39060
  if (n > 0)
38472
39061
  refs.add(n);
38473
39062
  }
38474
39063
  const fullRefStripPattern = /\b[\w.-]+\/[\w.-]+#\d+\b/gi;
39064
+ let inFence = false;
38475
39065
  for (const line of content.split("\n")) {
39066
+ if (/^\s*(```|~~~)/.test(line)) {
39067
+ inFence = !inFence;
39068
+ continue;
39069
+ }
39070
+ if (inFence)
39071
+ continue;
38476
39072
  if (/^\s*#+\s/.test(line))
38477
39073
  continue;
38478
- const stripped = line.replace(fullRefStripPattern, "");
39074
+ const stripped = line.replace(fullRefStripPattern, "").replace(/`[^`]*`/g, "");
38479
39075
  for (const m of stripped.matchAll(/(?:^|[^&\w])#(\d+)\b/g)) {
38480
39076
  const n = Number.parseInt(m[1], 10);
38481
39077
  if (n > 0)
@@ -38527,7 +39123,7 @@ async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
38527
39123
  }
38528
39124
 
38529
39125
  // ../core/dist/core/feature-discovery.js
38530
- var MODULE12 = "feature-discovery";
39126
+ var MODULE13 = "feature-discovery";
38531
39127
  var INTER_REPO_DELAY_MS = 2e3;
38532
39128
  var MIN_VIABILITY_SCORE = 40;
38533
39129
  var ANCHOR_THRESHOLD = 3;
@@ -38552,7 +39148,8 @@ function classifyHorizon(input) {
38552
39148
  function splitByHorizon(candidates, count, ratio = DEFAULT_SPLIT_RATIO) {
38553
39149
  const allQuick = candidates.filter((c) => c.horizon === "quick-win").sort((a, b) => b.viabilityScore - a.viabilityScore);
38554
39150
  const allBigger = candidates.filter((c) => c.horizon === "bigger-bet").sort((a, b) => b.viabilityScore - a.viabilityScore);
38555
- const targetQuick = Math.round(count * ratio);
39151
+ const clampedRatio = Math.max(0, Math.min(1, ratio));
39152
+ const targetQuick = Math.round(count * clampedRatio);
38556
39153
  const targetBigger = count - targetQuick;
38557
39154
  const quickTaken = Math.min(allQuick.length, targetQuick);
38558
39155
  const biggerTaken = Math.min(allBigger.length, targetBigger);
@@ -38606,6 +39203,17 @@ function detectWontfixNoContributor(input) {
38606
39203
  }
38607
39204
  var NO_ANCHORS_MESSAGE = "No anchor repos yet (need 3+ merged PRs in a repo). Try `scout search` to build relationships first.";
38608
39205
  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.";
39206
+ function buildRepoExclusionFilter(opts) {
39207
+ const excluded = new Set([...opts.excludeRepos ?? [], ...opts.aiPolicyBlocklist ?? []].map((r) => r.toLowerCase()));
39208
+ const excludedOrgs = new Set((opts.excludeOrgs ?? []).map((o) => o.toLowerCase()));
39209
+ return (repoFullName) => {
39210
+ const lower = repoFullName.toLowerCase();
39211
+ if (excluded.has(lower))
39212
+ return false;
39213
+ const org = lower.split("/")[0];
39214
+ return !(org && excludedOrgs.has(org));
39215
+ };
39216
+ }
38609
39217
  function extractLabels(item) {
38610
39218
  if (!Array.isArray(item.labels))
38611
39219
  return [];
@@ -38640,7 +39248,7 @@ async function vetAndClassify(item, vetter, roadmapRefs) {
38640
39248
  });
38641
39249
  } catch (err) {
38642
39250
  rethrowIfFatal(err);
38643
- warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
39251
+ warn(MODULE13, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
38644
39252
  return null;
38645
39253
  }
38646
39254
  const horizon = classifyHorizon({
@@ -38651,7 +39259,8 @@ async function vetAndClassify(item, vetter, roadmapRefs) {
38651
39259
  return { ...candidate, horizon };
38652
39260
  }
38653
39261
  async function discoverFeatures(opts) {
38654
- const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold);
39262
+ const includeRepo = buildRepoExclusionFilter(opts);
39263
+ const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold).filter(includeRepo);
38655
39264
  if (anchorRepos.length === 0) {
38656
39265
  return {
38657
39266
  quickWins: [],
@@ -38683,7 +39292,7 @@ async function discoverFeatures(opts) {
38683
39292
  roadmapRefs = refs;
38684
39293
  } catch (err) {
38685
39294
  rethrowIfFatal(err);
38686
- warn(MODULE12, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
39295
+ warn(MODULE13, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
38687
39296
  continue;
38688
39297
  }
38689
39298
  const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
@@ -38758,10 +39367,14 @@ async function discoverFeaturesBroad(opts) {
38758
39367
  merged.push(raw);
38759
39368
  }
38760
39369
  }
38761
- items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).slice(0, maxToVet);
39370
+ const includeRepo = buildRepoExclusionFilter(opts);
39371
+ items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).filter((it) => {
39372
+ const repo = extractRepoFromUrl(it.html_url);
39373
+ return repo === null || includeRepo(repo);
39374
+ }).slice(0, maxToVet);
38762
39375
  } catch (err) {
38763
39376
  rethrowIfFatal(err);
38764
- warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
39377
+ warn(MODULE13, `broad feature search failed: ${errorMessage(err)}`);
38765
39378
  return {
38766
39379
  quickWins: [],
38767
39380
  biggerBets: [],
@@ -38788,7 +39401,7 @@ async function discoverFeaturesBroad(opts) {
38788
39401
  // ../core/dist/core/gist-state-store.js
38789
39402
  var fs3 = __toESM(require("fs"), 1);
38790
39403
  var path3 = __toESM(require("path"), 1);
38791
- var MODULE13 = "gist-state";
39404
+ var MODULE14 = "gist-state";
38792
39405
  var GIST_DESCRIPTION = "oss-scout-state";
38793
39406
  var GIST_FILENAME = "state.json";
38794
39407
  var GIST_ID_FILE = "gist-id";
@@ -38833,7 +39446,7 @@ var GistStateStore = class {
38833
39446
  } catch (err) {
38834
39447
  if (getHttpStatusCode(err) === 401)
38835
39448
  throw err;
38836
- warn(MODULE13, `API bootstrap failed: ${errorMessage(err)}`);
39449
+ warn(MODULE14, `API bootstrap failed: ${errorMessage(err)}`);
38837
39450
  return this.bootstrapFromCache(classifyDegradedReason(err));
38838
39451
  }
38839
39452
  }
@@ -38842,24 +39455,28 @@ var GistStateStore = class {
38842
39455
  */
38843
39456
  async push(state) {
38844
39457
  if (!this.gistId) {
38845
- warn(MODULE13, "No gist ID \u2014 cannot push");
39458
+ warn(MODULE14, "No gist ID \u2014 cannot push");
38846
39459
  this.writeCache(state);
38847
39460
  return false;
38848
39461
  }
38849
39462
  let toWrite = state;
38850
39463
  try {
38851
39464
  const remote = await this.fetchGistState(this.gistId);
38852
- if (remote) {
38853
- toWrite = mergeStates(state, remote);
39465
+ if (remote.kind === "ok") {
39466
+ toWrite = mergeStates(state, remote.state);
39467
+ } else if (remote.kind === "invalid") {
39468
+ this.writeCache(state);
39469
+ warn(MODULE14, "Remote gist content failed validation \u2014 not overwriting it. Changes saved locally; gist sync skipped.");
39470
+ return false;
38854
39471
  }
38855
39472
  } catch (err) {
38856
39473
  rethrowIfFatal(err);
38857
- warn(MODULE13, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
39474
+ warn(MODULE14, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
38858
39475
  }
38859
39476
  this.writeCache(toWrite);
38860
39477
  const json2 = JSON.stringify(toWrite, null, 2);
38861
39478
  if (json2.length > 9e5) {
38862
- warn(MODULE13, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
39479
+ warn(MODULE14, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
38863
39480
  return false;
38864
39481
  }
38865
39482
  try {
@@ -38869,11 +39486,11 @@ var GistStateStore = class {
38869
39486
  [GIST_FILENAME]: { content: json2 }
38870
39487
  }
38871
39488
  });
38872
- debug(MODULE13, "State pushed to gist");
39489
+ debug(MODULE14, "State pushed to gist");
38873
39490
  return true;
38874
39491
  } catch (err) {
38875
39492
  rethrowIfFatal(err);
38876
- warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
39493
+ warn(MODULE14, `Failed to push: ${errorMessage(err)}`);
38877
39494
  return false;
38878
39495
  }
38879
39496
  }
@@ -38885,40 +39502,40 @@ var GistStateStore = class {
38885
39502
  async bootstrapFromApi() {
38886
39503
  const cachedId = this.readCachedGistId();
38887
39504
  if (cachedId) {
38888
- debug(MODULE13, `Trying cached gist ID: ${cachedId}`);
39505
+ debug(MODULE14, `Trying cached gist ID: ${cachedId}`);
38889
39506
  try {
38890
39507
  const fetched = await this.fetchGistState(cachedId);
38891
- if (fetched) {
39508
+ if (fetched.kind === "ok") {
38892
39509
  this.gistId = cachedId;
38893
- const state = this.mergeCacheInto(fetched);
39510
+ const state = this.mergeCacheInto(fetched.state);
38894
39511
  this.writeCache(state);
38895
39512
  return { gistId: cachedId, state, created: false };
38896
39513
  }
38897
39514
  } catch (err) {
38898
39515
  if (getHttpStatusCode(err) !== 404)
38899
39516
  throw err;
38900
- debug(MODULE13, `Cached gist gone (404): ${errorMessage(err)}`);
39517
+ debug(MODULE14, `Cached gist gone (404): ${errorMessage(err)}`);
38901
39518
  }
38902
- debug(MODULE13, "Cached gist ID invalid, searching...");
39519
+ debug(MODULE14, "Cached gist ID invalid, searching...");
38903
39520
  }
38904
39521
  const search = await this.searchForGist();
38905
39522
  if (search.id) {
38906
- debug(MODULE13, `Found gist via search: ${search.id}`);
39523
+ debug(MODULE14, `Found gist via search: ${search.id}`);
38907
39524
  this.saveGistId(search.id);
38908
39525
  this.gistId = search.id;
38909
39526
  const fetched = await this.fetchGistState(search.id);
38910
- if (fetched) {
38911
- const state = this.mergeCacheInto(fetched);
39527
+ if (fetched.kind === "ok") {
39528
+ const state = this.mergeCacheInto(fetched.state);
38912
39529
  this.writeCache(state);
38913
39530
  return { gistId: search.id, state, created: false };
38914
39531
  }
38915
- warn(MODULE13, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
39532
+ warn(MODULE14, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
38916
39533
  return this.bootstrapFromCache("unknown");
38917
39534
  }
38918
39535
  if (!search.exhaustive) {
38919
- warn(MODULE13, `Scanned the first ${SEARCH_MAX_PAGES * 100} gists without finding an oss-scout state gist, but the account has more. Creating a new state gist; if one already exists, copy its id into the gist-id file in the oss-scout data directory to avoid a duplicate.`);
39536
+ warn(MODULE14, `Scanned the first ${SEARCH_MAX_PAGES * 100} gists without finding an oss-scout state gist, but the account has more. Creating a new state gist; if one already exists, copy its id into the gist-id file in the oss-scout data directory to avoid a duplicate.`);
38920
39537
  }
38921
- debug(MODULE13, "No existing gist found, creating new one");
39538
+ debug(MODULE14, "No existing gist found, creating new one");
38922
39539
  const freshState = ScoutStateSchema.parse({ version: 1 });
38923
39540
  const newId = await this.createGist(freshState);
38924
39541
  this.saveGistId(newId);
@@ -38929,7 +39546,7 @@ var GistStateStore = class {
38929
39546
  bootstrapFromCache(reason) {
38930
39547
  const cached2 = this.readCache();
38931
39548
  if (cached2) {
38932
- debug(MODULE13, "Bootstrapped from local cache (degraded mode)");
39549
+ debug(MODULE14, "Bootstrapped from local cache (degraded mode)");
38933
39550
  const cachedId = this.readCachedGistId();
38934
39551
  if (cachedId)
38935
39552
  this.gistId = cachedId;
@@ -38941,7 +39558,7 @@ var GistStateStore = class {
38941
39558
  degradedReason: reason
38942
39559
  };
38943
39560
  }
38944
- debug(MODULE13, "No cache available, using fresh state (degraded mode)");
39561
+ debug(MODULE14, "No cache available, using fresh state (degraded mode)");
38945
39562
  const fresh = ScoutStateSchema.parse({ version: 1 });
38946
39563
  return {
38947
39564
  gistId: "",
@@ -38952,17 +39569,24 @@ var GistStateStore = class {
38952
39569
  };
38953
39570
  }
38954
39571
  // ── Gist API operations ──────────────────────────────────────────────
39572
+ /**
39573
+ * "missing" (no file/content) and "invalid" (content that fails parse or
39574
+ * schema validation) are distinct outcomes: an invalid remote may be a
39575
+ * corrupted-but-recoverable state or a newer state version written by a
39576
+ * newer binary, so callers must not treat it as "nothing there" and
39577
+ * overwrite it (#286).
39578
+ */
38955
39579
  async fetchGistState(gistId) {
38956
39580
  const { data } = await this.octokit.gists.get({ gist_id: gistId });
38957
39581
  const file2 = data.files?.[GIST_FILENAME];
38958
39582
  if (!file2?.content)
38959
- return null;
39583
+ return { kind: "missing" };
38960
39584
  try {
38961
39585
  const parsed = JSON.parse(file2.content);
38962
- return parseScoutState(parsed);
39586
+ return { kind: "ok", state: parseScoutState(parsed) };
38963
39587
  } catch (err) {
38964
- warn(MODULE13, `Gist content failed validation: ${errorMessage(err)}`);
38965
- return null;
39588
+ warn(MODULE14, `Gist content failed validation: ${errorMessage(err)}`);
39589
+ return { kind: "invalid" };
38966
39590
  }
38967
39591
  }
38968
39592
  /**
@@ -39004,7 +39628,7 @@ var GistStateStore = class {
39004
39628
  } catch (err) {
39005
39629
  const code = err?.code;
39006
39630
  if (code !== "ENOENT") {
39007
- warn(MODULE13, `Failed to read cached gist ID: ${errorMessage(err)}`);
39631
+ warn(MODULE14, `Failed to read cached gist ID: ${errorMessage(err)}`);
39008
39632
  }
39009
39633
  return null;
39010
39634
  }
@@ -39030,7 +39654,7 @@ var GistStateStore = class {
39030
39654
  } catch (err) {
39031
39655
  const code = err?.code;
39032
39656
  if (code !== "ENOENT") {
39033
- warn(MODULE13, `Failed to read state cache: ${errorMessage(err)}`);
39657
+ warn(MODULE14, `Failed to read state cache: ${errorMessage(err)}`);
39034
39658
  }
39035
39659
  return null;
39036
39660
  }
@@ -39041,7 +39665,7 @@ var GistStateStore = class {
39041
39665
  mode: 384
39042
39666
  });
39043
39667
  } catch (err) {
39044
- warn(MODULE13, `Failed to write cache: ${errorMessage(err)}`);
39668
+ warn(MODULE14, `Failed to write cache: ${errorMessage(err)}`);
39045
39669
  }
39046
39670
  }
39047
39671
  };
@@ -39056,7 +39680,7 @@ function mergeTombstones(local, remote) {
39056
39680
  }
39057
39681
  return [...byUrl.values()].filter((t) => {
39058
39682
  const ts = new Date(t.removedAt).getTime();
39059
- return !Number.isFinite(ts) || ts >= cutoff;
39683
+ return Number.isFinite(ts) && ts >= cutoff;
39060
39684
  });
39061
39685
  }
39062
39686
  function applyTombstones(items, tombstones, urlOf, touchedAtOf) {
@@ -39081,6 +39705,13 @@ function mergeStates(local, remote) {
39081
39705
  const localPrefsWin = localPrefsTs !== void 0 && (remotePrefsTs === void 0 || localPrefsTs > remotePrefsTs);
39082
39706
  const preferences = localPrefsWin ? local.preferences : remote.preferences;
39083
39707
  const preferencesUpdatedAt = pickFresherTimestamp(localPrefsTs, remotePrefsTs);
39708
+ const mergedPRsFinal = unionByUrl(local.mergedPRs, remote.mergedPRs);
39709
+ const closedPRsFinal = unionByUrl(local.closedPRs, remote.closedPRs);
39710
+ const resolvedUrls = /* @__PURE__ */ new Set([
39711
+ ...mergedPRsFinal.map((p) => p.url),
39712
+ ...closedPRsFinal.map((p) => p.url)
39713
+ ]);
39714
+ const openPRsFinal = unionByUrl(local.openPRs ?? [], remote.openPRs ?? []).filter((p) => !resolvedUrls.has(p.url));
39084
39715
  return {
39085
39716
  ...local,
39086
39717
  ...remote,
@@ -39091,18 +39722,33 @@ function mergeStates(local, remote) {
39091
39722
  repoScores: mergeRepoScores(local.repoScores, remote.repoScores),
39092
39723
  starredRepos: mergeStarredRepos(local, remote),
39093
39724
  starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
39094
- mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
39095
- closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
39096
- openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
39725
+ mergedPRs: mergedPRsFinal,
39726
+ closedPRs: closedPRsFinal,
39727
+ openPRs: openPRsFinal,
39097
39728
  savedResults: savedResultsFinal,
39098
39729
  skippedIssues,
39099
39730
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
39100
39731
  lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
39101
- gistId: remote.gistId ?? local.gistId
39732
+ gistId: remote.gistId ?? local.gistId,
39733
+ searchRotation: mergeSearchRotation(local.searchRotation, remote.searchRotation)
39102
39734
  };
39103
39735
  }
39736
+ function mergeSearchRotation(local, remote) {
39737
+ const localRotation = local ?? { languageOffset: 0 };
39738
+ const remoteRotation = remote ?? { languageOffset: 0 };
39739
+ const localTs = localRotation.lastRotatedAt;
39740
+ const remoteTs = remoteRotation.lastRotatedAt;
39741
+ if (localTs && remoteTs) {
39742
+ return localTs >= remoteTs ? localRotation : remoteRotation;
39743
+ }
39744
+ if (localTs)
39745
+ return localRotation;
39746
+ if (remoteTs)
39747
+ return remoteRotation;
39748
+ return remoteRotation.languageOffset >= localRotation.languageOffset ? remoteRotation : localRotation;
39749
+ }
39104
39750
  function mergeRepoScores(local, remote) {
39105
- const merged = { ...local };
39751
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), local);
39106
39752
  for (const [repo, remoteScore] of Object.entries(remote)) {
39107
39753
  const localScore = merged[repo];
39108
39754
  if (!localScore) {
@@ -39169,7 +39815,7 @@ function pickFresherTimestamp(a, b) {
39169
39815
  // ../core/dist/core/local-state.js
39170
39816
  var fs4 = __toESM(require("fs"), 1);
39171
39817
  var path4 = __toESM(require("path"), 1);
39172
- var MODULE14 = "local-state";
39818
+ var MODULE15 = "local-state";
39173
39819
  function getStatePath() {
39174
39820
  return path4.join(getDataDir(), "state.json");
39175
39821
  }
@@ -39183,13 +39829,13 @@ function loadLocalState() {
39183
39829
  if (code === "ENOENT") {
39184
39830
  return ScoutStateSchema.parse({ version: 1 });
39185
39831
  }
39186
- warn(MODULE14, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
39832
+ warn(MODULE15, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
39187
39833
  try {
39188
39834
  const backupPath = `${statePath}.corrupt.${Date.now()}`;
39189
39835
  fs4.copyFileSync(statePath, backupPath);
39190
- warn(MODULE14, `Corrupt state backed up to ${backupPath}`);
39836
+ warn(MODULE15, `Corrupt state backed up to ${backupPath}`);
39191
39837
  } catch (backupErr) {
39192
- warn(MODULE14, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
39838
+ warn(MODULE15, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
39193
39839
  }
39194
39840
  return ScoutStateSchema.parse({ version: 1 });
39195
39841
  }
@@ -39209,7 +39855,7 @@ function saveLocalState(state) {
39209
39855
  }
39210
39856
  throw err;
39211
39857
  }
39212
- debug(MODULE14, "State saved");
39858
+ debug(MODULE15, "State saved");
39213
39859
  }
39214
39860
 
39215
39861
  // ../core/dist/scout.js
@@ -39322,6 +39968,15 @@ var OssScout = class {
39322
39968
  this.evictStaleCacheEntries();
39323
39969
  this.cullExpiredSkips();
39324
39970
  const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
39971
+ if (options?.excludeRecentlySurfaced ?? true) {
39972
+ const ttlDays = options?.recentlySurfacedTtlDays ?? 7;
39973
+ const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1e3;
39974
+ for (const r of this.state.savedResults ?? []) {
39975
+ const seen = Date.parse(r.lastSeenAt);
39976
+ if (!Number.isNaN(seen) && seen >= cutoff)
39977
+ skippedUrls.add(r.issueUrl);
39978
+ }
39979
+ }
39325
39980
  const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
39326
39981
  const prefs = this.state.preferences;
39327
39982
  const prefLangs = prefs.preferLanguages ?? [];
@@ -39333,18 +39988,30 @@ var OssScout = class {
39333
39988
  const avoidRepos = options?.avoidRepos ?? (prefAvoid.length > 0 ? prefAvoid : void 0);
39334
39989
  const boostIssueTypes = options?.boostIssueTypes ?? (prefBoostTypes.length > 0 ? prefBoostTypes : void 0);
39335
39990
  const diversityRatio = options?.diversityRatio ?? prefs.diversityRatio ?? 0;
39336
- const { candidates, strategiesUsed } = await discovery.searchIssues({
39337
- maxResults: options?.maxResults,
39338
- strategies: options?.strategies,
39339
- skippedUrls,
39340
- preferLanguages,
39341
- preferRepos,
39342
- avoidRepos,
39343
- boostIssueTypes,
39344
- diversityRatio,
39345
- interPhaseDelayMs: options?.interPhaseDelayMs,
39346
- broadPhaseDelayMs: options?.broadPhaseDelayMs
39347
- });
39991
+ const rotation = this.state.searchRotation ?? { languageOffset: 0 };
39992
+ let candidates;
39993
+ let strategiesUsed;
39994
+ try {
39995
+ ({ candidates, strategiesUsed } = await discovery.searchIssues({
39996
+ maxResults: options?.maxResults,
39997
+ strategies: options?.strategies,
39998
+ skippedUrls,
39999
+ preferLanguages,
40000
+ preferRepos,
40001
+ avoidRepos,
40002
+ boostIssueTypes,
40003
+ diversityRatio,
40004
+ interPhaseDelayMs: options?.interPhaseDelayMs,
40005
+ broadPhaseDelayMs: options?.broadPhaseDelayMs,
40006
+ languageRotationOffset: rotation.languageOffset
40007
+ }));
40008
+ } catch (err) {
40009
+ if (err instanceof ValidationError) {
40010
+ this.advanceRotationIfBroadRan(rotation, err.strategiesUsed ?? []);
40011
+ }
40012
+ throw err;
40013
+ }
40014
+ this.advanceRotationIfBroadRan(rotation, strategiesUsed);
39348
40015
  for (const c of candidates) {
39349
40016
  this.updateRepoSignalsFromHealth(c.projectHealth);
39350
40017
  }
@@ -39358,6 +40025,23 @@ var OssScout = class {
39358
40025
  strategiesUsed
39359
40026
  };
39360
40027
  }
40028
+ /**
40029
+ * Advance the language-rotation cursor when the broad phase actually ran.
40030
+ * Called on both the success path and the zero-candidate ValidationError
40031
+ * path — a broad phase that ran and found nothing must still rotate, or the
40032
+ * same barren language slice would lead every subsequent run (#249
40033
+ * follow-up). Skipped/gated broad phases don't burn a rotation slot.
40034
+ */
40035
+ advanceRotationIfBroadRan(rotation, strategiesUsed) {
40036
+ if (!strategiesUsed.includes("broad"))
40037
+ return;
40038
+ this.state.searchRotation = {
40039
+ ...rotation,
40040
+ languageOffset: rotation.languageOffset + 1,
40041
+ lastRotatedAt: (/* @__PURE__ */ new Date()).toISOString()
40042
+ };
40043
+ this.dirty = true;
40044
+ }
39361
40045
  /**
39362
40046
  * Populate the `hasActiveMaintainers` repo-score signal from a freshly
39363
40047
  * computed projectHealth (#167). It was initialized false and never set, so
@@ -39412,12 +40096,16 @@ var OssScout = class {
39412
40096
  languages: this.state.preferences.languages,
39413
40097
  excludeRepos: this.state.preferences.excludeRepos,
39414
40098
  excludeOrgs: this.state.preferences.excludeOrgs,
40099
+ aiPolicyBlocklist: this.state.preferences.aiPolicyBlocklist,
39415
40100
  splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
39416
40101
  }) : await discoverFeatures({
39417
40102
  octokit,
39418
40103
  vetter,
39419
40104
  repoScores: this.state.repoScores ?? {},
39420
40105
  count,
40106
+ excludeRepos: this.state.preferences.excludeRepos,
40107
+ excludeOrgs: this.state.preferences.excludeOrgs,
40108
+ aiPolicyBlocklist: this.state.preferences.aiPolicyBlocklist,
39421
40109
  anchorThreshold: options?.anchorThreshold ?? this.state.preferences.featuresAnchorThreshold,
39422
40110
  splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
39423
40111
  });
@@ -39902,6 +40590,7 @@ var OssScout = class {
39902
40590
  }
39903
40591
  if (this.persistLocal) {
39904
40592
  try {
40593
+ this.state = mergeStates(this.state, loadLocalState());
39905
40594
  saveLocalState(this.state);
39906
40595
  } catch {
39907
40596
  return false;
@@ -40038,6 +40727,8 @@ function applyPreferenceField(preferences, key, value) {
40038
40727
  const prefs = { ...preferences };
40039
40728
  switch (field.type) {
40040
40729
  case "string":
40730
+ if (key === "slmTriageHost")
40731
+ assertValidTriageHost(value);
40041
40732
  prefs[key] = value;
40042
40733
  break;
40043
40734
  case "boolean":
@@ -40117,7 +40808,7 @@ function registerTools(server, scout) {
40117
40808
  "search",
40118
40809
  "Search for open source issues matching your preferences",
40119
40810
  {
40120
- maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
40811
+ maxResults: external_exports3.number().int().min(1).max(100).optional().describe("Maximum number of results to return (1-100, default 10)"),
40121
40812
  strategies: external_exports3.string().optional().describe(
40122
40813
  "Comma-separated search strategies: merged, starred, broad, maintained, all"
40123
40814
  ),
@@ -40147,7 +40838,16 @@ function registerTools(server, scout) {
40147
40838
  diversityRatio
40148
40839
  }) => {
40149
40840
  try {
40150
- const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
40841
+ const parsedStrategies = strategies ? strategies.split(",").map((s) => {
40842
+ const trimmed = s.trim();
40843
+ const parsed = SearchStrategySchema.safeParse(trimmed);
40844
+ if (!parsed.success) {
40845
+ throw new Error(
40846
+ `Invalid strategy "${trimmed}". Valid strategies: ${SearchStrategySchema.options.join(", ")}`
40847
+ );
40848
+ }
40849
+ return parsed.data;
40850
+ }) : void 0;
40151
40851
  const result = await withTimeout(
40152
40852
  scout.search({
40153
40853
  maxResults: maxResults ?? 10,
@@ -40190,7 +40890,7 @@ function registerTools(server, scout) {
40190
40890
  "scout-features",
40191
40891
  "Surface feature-scoped contribution opportunities in repos where you have 3+ merged PRs",
40192
40892
  {
40193
- maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
40893
+ maxResults: external_exports3.number().int().min(1).max(50).optional().describe("Maximum number of results to return (1-50, default 10)"),
40194
40894
  anchorThreshold: external_exports3.number().int().min(1).max(50).optional().describe("Anchor threshold override (default 3)"),
40195
40895
  splitRatio: external_exports3.number().min(0).max(1).optional().describe("Quick-wins/bigger-bets split ratio (default 0.6)")
40196
40896
  },
@@ -40457,7 +41157,7 @@ function registerResources(server, scout) {
40457
41157
  // src/index.ts
40458
41158
  var SERVER_INFO = {
40459
41159
  name: "oss-scout-mcp",
40460
- version: "0.10.0"
41160
+ version: "0.10.2"
40461
41161
  // x-release-please-version
40462
41162
  };
40463
41163
  function createServer(scout) {