@oss-scout/mcp 0.10.0 → 0.10.1

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;
@@ -37297,15 +37741,23 @@ function isValidTriageShape(value) {
37297
37741
  }
37298
37742
 
37299
37743
  // ../core/dist/core/issue-vetting.js
37300
- var MODULE9 = "issue-vetting";
37744
+ var MODULE10 = "issue-vetting";
37301
37745
  var MAX_CONCURRENT_VETTING = 3;
37302
37746
  var VETTING_CACHE_TTL_MS = 15 * 60 * 1e3;
37747
+ function repoAcceptsContributionsFromHealth(projectHealth) {
37748
+ if (projectHealth.checkFailed || projectHealth.recentMergedPRCount == null) {
37749
+ return null;
37750
+ }
37751
+ if (projectHealth.recentMergedPRCount > 0)
37752
+ return true;
37753
+ return projectHealth.recentMergeRate == null ? null : false;
37754
+ }
37303
37755
  function deriveRecommendation(input) {
37304
37756
  const notes = [];
37305
37757
  const reasonsToApprove = [];
37306
37758
  const reasonsToSkip = [];
37307
37759
  if (!input.noExistingPR)
37308
- notes.push(input.ownPR ? "Your PR is already in flight for this issue" : "Existing PR found for this issue");
37760
+ 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
37761
  if (!input.notClaimed)
37310
37762
  notes.push("Issue appears to be claimed by someone");
37311
37763
  if (input.existingPRInconclusive) {
@@ -37323,14 +37775,28 @@ function deriveRecommendation(input) {
37323
37775
  notes.push("Issue requirements are unclear");
37324
37776
  if (!input.contributionGuidelinesFound)
37325
37777
  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");
37778
+ if (input.repoAcceptsContributions === false)
37779
+ notes.push("Repo has merged no PRs in the last 90 days");
37780
+ else if (input.repoAcceptsContributions === null)
37781
+ notes.push("Could not verify whether the repo merges PRs");
37782
+ if (!input.noExistingPR) {
37783
+ if (input.ownPR)
37784
+ reasonsToSkip.push("You already have a PR in flight");
37785
+ else if (input.linkedPRMerged)
37786
+ reasonsToSkip.push("Linked PR already merged");
37787
+ else if (input.linkedPRClosed)
37788
+ reasonsToSkip.push("Linked PR closed without merge");
37789
+ else
37790
+ reasonsToSkip.push("Has existing PR");
37791
+ }
37328
37792
  if (!input.notClaimed)
37329
37793
  reasonsToSkip.push("Already claimed");
37330
37794
  if (!input.projectIsActive && !input.projectCheckFailed)
37331
37795
  reasonsToSkip.push("Inactive project");
37332
37796
  if (!input.clearRequirements)
37333
37797
  reasonsToSkip.push("Unclear requirements");
37798
+ if (input.repoAcceptsContributions === false)
37799
+ reasonsToSkip.push("Repo has no recent merged PRs");
37334
37800
  if (input.noExistingPR)
37335
37801
  reasonsToApprove.push("No existing PR");
37336
37802
  if (input.notClaimed)
@@ -37356,16 +37822,18 @@ function deriveRecommendation(input) {
37356
37822
  let recommendation;
37357
37823
  if (input.issueClosed) {
37358
37824
  recommendation = "skip";
37825
+ } else if (input.linkedPRMerged || input.linkedPRClosed) {
37826
+ recommendation = "skip";
37359
37827
  } else if (input.ownPR) {
37360
37828
  recommendation = "skip";
37361
- } else if (input.passedAllChecks) {
37829
+ } else if (input.passedAllChecks && input.repoAcceptsContributions !== false) {
37362
37830
  recommendation = "approve";
37363
37831
  } else if (reasonsToSkip.length > 2) {
37364
37832
  recommendation = "skip";
37365
37833
  } else {
37366
37834
  recommendation = "needs_review";
37367
37835
  }
37368
- const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive;
37836
+ const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive || input.repoAcceptsContributions === null;
37369
37837
  if (recommendation === "approve" && hasInconclusiveChecks) {
37370
37838
  recommendation = "needs_review";
37371
37839
  notes.push("Recommendation downgraded: one or more checks were inconclusive");
@@ -37403,7 +37871,7 @@ var IssueVetter = class {
37403
37871
  const cacheKey = versionedCacheKey(`vet:${issueUrl}${sigKey}`);
37404
37872
  const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
37405
37873
  if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
37406
- debug(MODULE9, `Vetting cache hit for ${issueUrl}`);
37874
+ debug(MODULE10, `Vetting cache hit for ${issueUrl}`);
37407
37875
  return cached2;
37408
37876
  }
37409
37877
  const parsed = parseGitHubUrl(issueUrl);
@@ -37418,7 +37886,7 @@ var IssueVetter = class {
37418
37886
  const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
37419
37887
  checkNoExistingPR(this.octokit, owner, repo, number4),
37420
37888
  checkNotClaimed(this.octokit, owner, repo, number4, core.commentCount),
37421
- checkProjectHealth(this.octokit, owner, repo),
37889
+ checkProjectHealth(this.octokit, owner, repo, this.budgetTracker),
37422
37890
  fetchContributionGuidelines(this.octokit, owner, repo),
37423
37891
  hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo, this.budgetTracker)
37424
37892
  ]);
@@ -37428,8 +37896,11 @@ var IssueVetter = class {
37428
37896
  const username = this.stateReader.getGitHubUsername?.() ?? "";
37429
37897
  const linkedPR = existingPRCheck.linkedPR;
37430
37898
  const ownPR = !noExistingPR && !!linkedPR && linkedPR.state === "open" && !linkedPR.merged && username !== "" && linkedPR.author.toLowerCase() === username.toLowerCase();
37899
+ const linkedPRMerged = !!linkedPR && linkedPR.merged;
37900
+ const linkedPRClosed = !!linkedPR && linkedPR.state === "closed" && !linkedPR.merged;
37431
37901
  const clearRequirements = analyzeRequirements(core.body);
37432
37902
  const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
37903
+ const repoAcceptsContributions = repoAcceptsContributionsFromHealth(projectHealth);
37433
37904
  const vettingResult = {
37434
37905
  passedAllChecks: noExistingPR && notClaimed && projectActive && clearRequirements,
37435
37906
  checks: {
@@ -37465,10 +37936,12 @@ var IssueVetter = class {
37465
37936
  }
37466
37937
  const matchesCategory = repoBelongsToCategory(repoFullName, this.stateReader.getProjectCategories());
37467
37938
  const issueClosed = core.state === "closed";
37468
- const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive;
37939
+ const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive || repoAcceptsContributions === null;
37469
37940
  const { notes, reasonsToApprove, reasonsToSkip, recommendation } = deriveRecommendation({
37470
37941
  noExistingPR,
37471
37942
  ownPR,
37943
+ linkedPRMerged,
37944
+ linkedPRClosed,
37472
37945
  notClaimed,
37473
37946
  clearRequirements,
37474
37947
  contributionGuidelinesFound: !!contributionGuidelines,
@@ -37485,7 +37958,8 @@ var IssueVetter = class {
37485
37958
  orgHasMergedPRs,
37486
37959
  matchesCategory,
37487
37960
  issueClosed,
37488
- passedAllChecks: vettingResult.passedAllChecks
37961
+ passedAllChecks: vettingResult.passedAllChecks,
37962
+ repoAcceptsContributions
37489
37963
  });
37490
37964
  vettingResult.notes = notes;
37491
37965
  const repoQualityBonus = projectHealth.checkFailed ? 0 : calculateRepoQualityBonus(projectHealth.stargazersCount ?? 0, projectHealth.forksCount ?? 0);
@@ -37565,6 +38039,36 @@ var IssueVetter = class {
37565
38039
  updatedAt: ghIssue.updated_at
37566
38040
  };
37567
38041
  }
38042
+ /**
38043
+ * Batch-prefetch merged-PR counts for the repos in a vetting batch (#182),
38044
+ * warming the cache `checkUserMergedPRsInRepo` reads so each per-repo Search
38045
+ * API call becomes a cache hit.
38046
+ *
38047
+ * Two repos are intentionally excluded:
38048
+ * - Repos already known to have the user's merged PRs
38049
+ * (`getReposWithMergedPRs`): `vetIssue` short-circuits those to a merged
38050
+ * count of 1 and never calls the Search API, so prefetching them is waste.
38051
+ * - Everything, when the GitHub username is unknown: the GraphQL `author:`
38052
+ * filter needs a concrete login and the REST path falls back to `@me`.
38053
+ */
38054
+ async prefetchMergedPRCounts(issueRefs) {
38055
+ const username = this.stateReader.getGitHubUsername?.() ?? "";
38056
+ if (!username)
38057
+ return;
38058
+ const alreadyKnown = new Set(this.stateReader.getReposWithMergedPRs());
38059
+ const seen = /* @__PURE__ */ new Set();
38060
+ const repos = [];
38061
+ for (const { owner, repo } of issueRefs) {
38062
+ const full = `${owner}/${repo}`;
38063
+ if (alreadyKnown.has(full) || seen.has(full))
38064
+ continue;
38065
+ seen.add(full);
38066
+ repos.push({ owner, repo });
38067
+ }
38068
+ if (repos.length === 0)
38069
+ return;
38070
+ await prefetchMergedPRCounts(this.octokit, username, repos);
38071
+ }
37568
38072
  /**
37569
38073
  * Vet multiple issues in parallel with concurrency limit
37570
38074
  */
@@ -37576,10 +38080,14 @@ var IssueVetter = class {
37576
38080
  let attemptedCount = 0;
37577
38081
  let firstAuthError = null;
37578
38082
  const uniqueUrls = [...new Set(urls)];
37579
- const prefetched = await prefetchIssueCores(this.octokit, uniqueUrls.flatMap((url2) => {
38083
+ const issueRefs = uniqueUrls.flatMap((url2) => {
37580
38084
  const p = parseGitHubUrl(url2);
37581
38085
  return p && p.type === "issues" ? [{ owner: p.owner, repo: p.repo, number: p.number }] : [];
37582
- }));
38086
+ });
38087
+ const [prefetched] = await Promise.all([
38088
+ prefetchIssueCores(this.octokit, issueRefs),
38089
+ this.prefetchMergedPRCounts(issueRefs)
38090
+ ]);
37583
38091
  const prefetchFor = (url2) => {
37584
38092
  const p = parseGitHubUrl(url2);
37585
38093
  return p && p.type === "issues" ? prefetched.get(issueCoreKey(p.owner, p.repo, p.number)) : void 0;
@@ -37599,7 +38107,7 @@ var IssueVetter = class {
37599
38107
  candidates.push(candidate);
37600
38108
  }
37601
38109
  }).catch((error48) => {
37602
- if (getHttpStatusCode(error48) === 401) {
38110
+ if (isAuthError(error48)) {
37603
38111
  firstAuthError ??= error48;
37604
38112
  return;
37605
38113
  }
@@ -37607,7 +38115,7 @@ var IssueVetter = class {
37607
38115
  if (isRateLimitError(error48)) {
37608
38116
  rateLimitFailures++;
37609
38117
  }
37610
- warn(MODULE9, `Error vetting issue ${url2}:`, errorMessage(error48));
38118
+ warn(MODULE10, `Error vetting issue ${url2}:`, errorMessage(error48));
37611
38119
  }).finally(() => pending.delete(url2));
37612
38120
  pending.set(url2, task);
37613
38121
  if (pending.size >= MAX_CONCURRENT_VETTING) {
@@ -37617,13 +38125,13 @@ var IssueVetter = class {
37617
38125
  await Promise.allSettled(pending.values());
37618
38126
  if (firstAuthError) {
37619
38127
  if (candidates.length > 0) {
37620
- warn(MODULE9, `Auth failed mid-batch after ${candidates.length} successful vet(s) \u2014 discarding partial results`);
38128
+ warn(MODULE10, `Auth failed mid-batch after ${candidates.length} successful vet(s) \u2014 discarding partial results`);
37621
38129
  }
37622
38130
  throw firstAuthError;
37623
38131
  }
37624
38132
  const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
37625
38133
  if (allFailed) {
37626
- warn(MODULE9, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
38134
+ warn(MODULE10, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
37627
38135
  }
37628
38136
  return {
37629
38137
  candidates: candidates.slice(0, maxResults),
@@ -37634,14 +38142,22 @@ var IssueVetter = class {
37634
38142
  };
37635
38143
 
37636
38144
  // ../core/dist/core/search-phases.js
37637
- var MODULE10 = "search-phases";
38145
+ var MODULE11 = "search-phases";
37638
38146
  var GITHUB_MAX_BOOLEAN_OPS = 5;
37639
38147
  var INTER_QUERY_DELAY_MS = 2e3;
38148
+ var GRAPHQL_INTER_QUERY_DELAY_MS = 500;
38149
+ var graphqlSearchQueryCount = 0;
38150
+ function getGraphQLSearchQueryCount() {
38151
+ return graphqlSearchQueryCount;
38152
+ }
38153
+ function resetGraphQLSearchQueryCount() {
38154
+ graphqlSearchQueryCount = 0;
38155
+ }
37640
38156
  function chunkLabels(labels, reservedOps = 0) {
37641
38157
  const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
37642
38158
  if (maxPerChunk < 1) {
37643
38159
  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.`);
38160
+ 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
38161
  }
37646
38162
  return [[]];
37647
38163
  }
@@ -37651,7 +38167,7 @@ function chunkLabels(labels, reservedOps = 0) {
37651
38167
  for (let i = 0; i < labels.length; i += maxPerChunk) {
37652
38168
  chunks.push(labels.slice(i, i + maxPerChunk));
37653
38169
  }
37654
- debug(MODULE10, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
38170
+ debug(MODULE11, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
37655
38171
  return chunks;
37656
38172
  }
37657
38173
  function buildLabelQuery(labels) {
@@ -37688,7 +38204,7 @@ async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTrac
37688
38204
  const cache = getHttpCache();
37689
38205
  const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
37690
38206
  if (cached2) {
37691
- debug(MODULE10, `Search cache hit for query`);
38207
+ debug(MODULE11, `Search cache hit for query`);
37692
38208
  return cached2;
37693
38209
  }
37694
38210
  await tracker.waitForBudget();
@@ -37702,10 +38218,32 @@ async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTrac
37702
38218
  if (data.items.length > 0) {
37703
38219
  cache.set(cacheKey, "", data);
37704
38220
  } else {
37705
- debug(MODULE10, `Skipping cache for empty search result (possible rate limit artifact)`);
38221
+ debug(MODULE11, `Skipping cache for empty search result (possible rate limit artifact)`);
37706
38222
  }
37707
38223
  return data;
37708
38224
  }
38225
+ async function searchIssuesGraphQLFirst(octokit, params, tracker = getSearchBudgetTracker()) {
38226
+ const cacheKey = versionedCacheKey(`gqlsearch:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
38227
+ const cache = getHttpCache();
38228
+ const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
38229
+ if (cached2) {
38230
+ debug(MODULE11, `GraphQL search cache hit for query`);
38231
+ return cached2;
38232
+ }
38233
+ const gqlQuery = `${params.q} sort:${params.sort}-${params.order}`;
38234
+ const gql = await graphqlSearchIssues(octokit, gqlQuery, Math.min(params.per_page, 100));
38235
+ if (gql) {
38236
+ graphqlSearchQueryCount++;
38237
+ if (gql.items.length > 0) {
38238
+ cache.set(cacheKey, "", gql);
38239
+ } else {
38240
+ debug(MODULE11, `Skipping cache for empty GraphQL search result (possible transient artifact)`);
38241
+ }
38242
+ return gql;
38243
+ }
38244
+ warn(MODULE11, `GraphQL broad search unavailable; falling back to REST search API`);
38245
+ return cachedSearchIssues(octokit, params, tracker);
38246
+ }
37709
38247
  async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResults) {
37710
38248
  const items = [];
37711
38249
  for (const repoFullName of repos) {
@@ -37750,15 +38288,15 @@ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResul
37750
38288
  if (getHttpStatusCode(error48) === 401)
37751
38289
  throw error48;
37752
38290
  if (isRateLimitError(error48)) {
37753
- warn(MODULE10, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
38291
+ warn(MODULE11, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
37754
38292
  break;
37755
38293
  }
37756
- warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
38294
+ warn(MODULE11, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
37757
38295
  }
37758
38296
  }
37759
38297
  return items;
37760
38298
  }
37761
- async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn) {
38299
+ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn, perPage = 5) {
37762
38300
  const candidates = [];
37763
38301
  let failedRepos = 0;
37764
38302
  let rateLimitFailures = 0;
@@ -37780,7 +38318,7 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
37780
38318
  state: "open",
37781
38319
  sort: "created",
37782
38320
  direction: "desc",
37783
- per_page: 5,
38321
+ per_page: perPage,
37784
38322
  ...label !== void 0 ? { labels: label } : {}
37785
38323
  });
37786
38324
  for (const issue2 of response.data) {
@@ -37815,13 +38353,13 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
37815
38353
  if (isRateLimitError(error48)) {
37816
38354
  rateLimitFailures++;
37817
38355
  }
37818
- warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
38356
+ warn(MODULE11, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
37819
38357
  }
37820
38358
  }
37821
38359
  const allReposFailed = failedRepos === repos.length && repos.length > 0;
37822
38360
  const rateLimitHit = rateLimitFailures > 0;
37823
38361
  if (allReposFailed) {
37824
- warn(MODULE10, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
38362
+ warn(MODULE11, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
37825
38363
  }
37826
38364
  return { candidates, allReposFailed, rateLimitHit };
37827
38365
  }
@@ -37831,9 +38369,9 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
37831
38369
  const allItems = [];
37832
38370
  for (let i = 0; i < labelChunks.length; i++) {
37833
38371
  if (i > 0)
37834
- await sleep(INTER_QUERY_DELAY_MS);
38372
+ await sleep(GRAPHQL_INTER_QUERY_DELAY_MS);
37835
38373
  const query = buildQuery(buildLabelQuery(labelChunks[i]));
37836
- const data = await cachedSearchIssues(octokit, {
38374
+ const data = await searchIssuesGraphQLFirst(octokit, {
37837
38375
  q: query,
37838
38376
  sort: "created",
37839
38377
  order: "desc",
@@ -37857,13 +38395,15 @@ function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
37857
38395
  return [languages.map((l) => `language:${l}`).join(" ")];
37858
38396
  return languages.map((l) => `language:${l}`);
37859
38397
  }
37860
- async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker()) {
37861
- const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
38398
+ async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker(), startOffset = 0) {
38399
+ const variants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
38400
+ const k = variants.length > 0 ? startOffset % variants.length : 0;
38401
+ const langVariants = [...variants.slice(k), ...variants.slice(0, k)];
37862
38402
  const seenUrls = /* @__PURE__ */ new Set();
37863
38403
  const allItems = [];
37864
38404
  for (let i = 0; i < langVariants.length; i++) {
37865
38405
  if (i > 0)
37866
- await sleep(INTER_QUERY_DELAY_MS);
38406
+ await sleep(GRAPHQL_INTER_QUERY_DELAY_MS);
37867
38407
  const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage, tracker);
37868
38408
  for (const item of items) {
37869
38409
  if (!seenUrls.has(item.html_url)) {
@@ -37878,7 +38418,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37878
38418
  const spamRepos = detectLabelFarmingRepos(items);
37879
38419
  if (spamRepos.size > 0) {
37880
38420
  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(", ")}`);
38421
+ debug(MODULE11, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
37882
38422
  }
37883
38423
  const itemsToVet = filterIssues(items).filter((item) => {
37884
38424
  const repoFullName = extractRepoFromUrl(item.repository_url);
@@ -37889,7 +38429,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37889
38429
  return excludedRepoSets.every((s) => !s.has(repoFullName));
37890
38430
  }).slice(0, remainingNeeded * 2);
37891
38431
  if (itemsToVet.length === 0) {
37892
- debug(MODULE10, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
38432
+ debug(MODULE11, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
37893
38433
  return { candidates: [], allVetFailed: false, rateLimitHit: false };
37894
38434
  }
37895
38435
  const { candidates: results, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
@@ -37901,7 +38441,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37901
38441
  });
37902
38442
  const starFilteredCount = results.length - starFiltered.length;
37903
38443
  if (starFilteredCount > 0) {
37904
- debug(MODULE10, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
38444
+ debug(MODULE11, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
37905
38445
  }
37906
38446
  return { candidates: starFiltered, allVetFailed, rateLimitHit };
37907
38447
  }
@@ -37994,9 +38534,12 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
37994
38534
  }
37995
38535
 
37996
38536
  // ../core/dist/core/issue-discovery.js
37997
- var MODULE11 = "issue-discovery";
38537
+ var MODULE12 = "issue-discovery";
37998
38538
  var LOW_BUDGET_THRESHOLD = 20;
37999
38539
  var CRITICAL_BUDGET_THRESHOLD = 10;
38540
+ var PHASE0_PER_PAGE = 30;
38541
+ var CONTRIBUTED_REPO_MAX_AGE_DAYS = 365;
38542
+ var PHASE0_MAX_SHARE = 0.5;
38000
38543
  function buildIssueFilter(config2) {
38001
38544
  return (items) => {
38002
38545
  return items.filter((item) => {
@@ -38018,6 +38561,8 @@ function buildIssueFilter(config2) {
38018
38561
  if (config2.skippedUrls.has(item.html_url))
38019
38562
  return false;
38020
38563
  const updatedAt = new Date(item.updated_at);
38564
+ if (Number.isNaN(updatedAt.getTime()))
38565
+ return false;
38021
38566
  const ageDays = daysBetween(updatedAt, config2.now);
38022
38567
  if (ageDays > config2.maxAgeDays)
38023
38568
  return false;
@@ -38028,9 +38573,9 @@ function buildIssueFilter(config2) {
38028
38573
  };
38029
38574
  }
38030
38575
  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`);
38576
+ info(MODULE12, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter, ${PHASE0_PER_PAGE}/repo)...`);
38577
+ const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues, PHASE0_PER_PAGE);
38578
+ info(MODULE12, `Found ${candidates.length} candidates from merged-PR repos`);
38034
38579
  return {
38035
38580
  candidates,
38036
38581
  error: allReposFailed ? "All merged-PR repo fetches failed" : null,
@@ -38038,26 +38583,26 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
38038
38583
  };
38039
38584
  }
38040
38585
  async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
38041
- info(MODULE11, `Phase 1: Searching issues in ${repos.length} starred repos...`);
38586
+ info(MODULE12, `Phase 1: Searching issues in ${repos.length} starred repos...`);
38042
38587
  const phase1Labels = labels.slice(0, 3);
38043
38588
  const reposToSearch = repos.slice(0, 10);
38044
38589
  const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
38045
- info(MODULE11, `Found ${candidates.length} candidates from starred repos`);
38590
+ info(MODULE12, `Found ${candidates.length} candidates from starred repos`);
38046
38591
  return {
38047
38592
  candidates,
38048
38593
  error: allReposFailed ? "All starred repo fetches failed" : null,
38049
38594
  rateLimitHit
38050
38595
  };
38051
38596
  }
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...");
38597
+ async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues, tracker, languageRotationOffset = 0) {
38598
+ info(MODULE12, "Phase 2: General issue search...");
38054
38599
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38055
38600
  const tierLabelGroups = [];
38056
38601
  if (scopes && scopes.length > 1) {
38057
38602
  for (const scope of scopes) {
38058
38603
  const scopeLabels = SCOPE_LABELS[scope] ?? [];
38059
38604
  if (scopeLabels.length === 0) {
38060
- warn(MODULE11, `Scope "${scope}" has no labels, skipping tier`);
38605
+ warn(MODULE12, `Scope "${scope}" has no labels, skipping tier`);
38061
38606
  continue;
38062
38607
  }
38063
38608
  tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
@@ -38076,8 +38621,8 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38076
38621
  let rateLimitHit = false;
38077
38622
  for (const { tier, tierLabels } of tierLabelGroups) {
38078
38623
  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...`);
38624
+ const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3, tracker, languageRotationOffset);
38625
+ info(MODULE12, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
38081
38626
  const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
38082
38627
  tierResults.push(tierCandidates);
38083
38628
  for (const c of tierCandidates)
@@ -38088,7 +38633,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38088
38633
  if (vetRateLimitHit) {
38089
38634
  rateLimitHit = true;
38090
38635
  }
38091
- info(MODULE11, `Found ${tierCandidates.length} candidates from ${tier} tier`);
38636
+ info(MODULE12, `Found ${tierCandidates.length} candidates from ${tier} tier`);
38092
38637
  } catch (err) {
38093
38638
  if (getHttpStatusCode(err) === 401)
38094
38639
  throw err;
@@ -38097,13 +38642,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38097
38642
  if (isRateLimitError(err)) {
38098
38643
  rateLimitHit = true;
38099
38644
  }
38100
- warn(MODULE11, `Error in ${tier} tier search: ${errMsg}`);
38645
+ warn(MODULE12, `Error in ${tier} tier search: ${errMsg}`);
38101
38646
  tierResults.push([]);
38102
38647
  }
38103
38648
  }
38104
38649
  const interleaved = interleaveArrays(tierResults);
38105
38650
  if (interleaved.length === 0 && error48) {
38106
- warn(MODULE11, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
38651
+ warn(MODULE12, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
38107
38652
  }
38108
38653
  return {
38109
38654
  candidates: interleaved.slice(0, maxResults),
@@ -38112,17 +38657,17 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38112
38657
  };
38113
38658
  }
38114
38659
  async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues, tracker) {
38115
- info(MODULE11, "Phase 3: Searching actively maintained repos...");
38660
+ info(MODULE12, "Phase 3: Searching actively maintained repos...");
38116
38661
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38117
38662
  const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
38118
38663
  if (eligibleStarred.length > 0) {
38119
- info(MODULE11, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
38664
+ info(MODULE12, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
38120
38665
  const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
38121
38666
  if (restItems.length > 0) {
38122
38667
  try {
38123
38668
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
38124
38669
  if (candidates.length > 0) {
38125
- info(MODULE11, `Found ${candidates.length} candidates from maintained-repo REST search`);
38670
+ info(MODULE12, `Found ${candidates.length} candidates from maintained-repo REST search`);
38126
38671
  return {
38127
38672
  candidates,
38128
38673
  error: allVetFailed ? "all vetting failed" : null,
@@ -38132,11 +38677,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38132
38677
  } catch (error48) {
38133
38678
  if (getHttpStatusCode(error48) === 401)
38134
38679
  throw error48;
38135
- warn(MODULE11, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
38680
+ warn(MODULE12, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
38136
38681
  }
38137
38682
  }
38138
38683
  }
38139
- info(MODULE11, "Phase 3: Falling back to Search API...");
38684
+ info(MODULE12, "Phase 3: Falling back to Search API...");
38140
38685
  const thirtyDaysAgo = /* @__PURE__ */ new Date();
38141
38686
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
38142
38687
  const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
@@ -38144,15 +38689,15 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38144
38689
  const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : "";
38145
38690
  const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
38146
38691
  try {
38147
- const data = await cachedSearchIssues(octokit, {
38692
+ const data = await searchIssuesGraphQLFirst(octokit, {
38148
38693
  q: phase3Query,
38149
38694
  sort: "updated",
38150
38695
  order: "desc",
38151
38696
  per_page: maxResults * 3
38152
38697
  }, tracker);
38153
- info(MODULE11, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
38698
+ info(MODULE12, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
38154
38699
  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`);
38700
+ info(MODULE12, `Found ${candidates.length} candidates from maintained-repo search`);
38156
38701
  return {
38157
38702
  candidates,
38158
38703
  error: allVetFailed ? "all vetting failed" : null,
@@ -38162,7 +38707,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38162
38707
  if (getHttpStatusCode(error48) === 401)
38163
38708
  throw error48;
38164
38709
  const errMsg = errorMessage(error48);
38165
- warn(MODULE11, `Error in maintained-repo search: ${errMsg}`);
38710
+ warn(MODULE12, `Error in maintained-repo search: ${errMsg}`);
38166
38711
  return {
38167
38712
  candidates: [],
38168
38713
  error: errMsg,
@@ -38246,7 +38791,7 @@ var IssueDiscovery = class {
38246
38791
  let rateLimitHitDuringSearch = false;
38247
38792
  const applyInterPhaseDelay = async () => {
38248
38793
  if (interPhaseDelay > 0) {
38249
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38794
+ info(MODULE12, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38250
38795
  await sleep(interPhaseDelay);
38251
38796
  }
38252
38797
  };
@@ -38256,9 +38801,11 @@ var IssueDiscovery = class {
38256
38801
  if (result.rateLimitHit)
38257
38802
  rateLimitHitDuringSearch = true;
38258
38803
  };
38804
+ const viableCandidateCount = () => allCandidates.filter((c) => c.recommendation !== "skip").length;
38259
38805
  this.rateLimitWarning = null;
38806
+ resetGraphQLSearchQueryCount();
38260
38807
  const tracker = this.budgetTracker;
38261
- let searchBudget = LOW_BUDGET_THRESHOLD - 1;
38808
+ let searchBudget = CRITICAL_BUDGET_THRESHOLD - 1;
38262
38809
  try {
38263
38810
  const rateLimit = await checkRateLimit(this.githubToken);
38264
38811
  searchBudget = rateLimit.remaining;
@@ -38266,22 +38813,22 @@ var IssueDiscovery = class {
38266
38813
  if (rateLimit.remaining < 5) {
38267
38814
  const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
38268
38815
  this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
38269
- warn(MODULE11, this.rateLimitWarning);
38816
+ warn(MODULE12, this.rateLimitWarning);
38270
38817
  }
38271
38818
  if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
38272
- info(MODULE11, `Search budget critical (${searchBudget} remaining) \u2014 running only Phase 0`);
38819
+ info(MODULE12, `Search budget critical (${searchBudget} remaining) \u2014 skipping the starred phase; broad/maintained still run on GraphQL (points bucket)`);
38273
38820
  } else if (searchBudget < LOW_BUDGET_THRESHOLD) {
38274
- info(MODULE11, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
38821
+ info(MODULE12, `Search budget low (${searchBudget} remaining) \u2014 broad/maintained run on GraphQL (points bucket), unaffected by REST search quota`);
38275
38822
  }
38276
38823
  } catch (error48) {
38277
38824
  if (getHttpStatusCode(error48) === 401)
38278
38825
  throw error48;
38279
38826
  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));
38827
+ warn(MODULE12, "Could not check rate limit \u2014 using conservative budget, skipping the starred phase:", errorMessage(error48));
38281
38828
  }
38282
38829
  if (searchBudget <= 0) {
38283
- this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
38284
- return { candidates: [], strategiesUsed: [] };
38830
+ this.rateLimitWarning = "GitHub search API quota exhausted \u2014 skipping the starred phase; other phases don't use it.";
38831
+ warn(MODULE12, this.rateLimitWarning);
38285
38832
  }
38286
38833
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
38287
38834
  const openPRRepos = this.stateReader.getReposWithOpenPRs();
@@ -38292,17 +38839,24 @@ var IssueDiscovery = class {
38292
38839
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
38293
38840
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist.map((r) => r.toLowerCase()));
38294
38841
  if (aiBlocklisted.size > 0) {
38295
- debug(MODULE11, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
38842
+ debug(MODULE12, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
38296
38843
  }
38297
- const filterIssues = buildIssueFilter({
38844
+ const baseFilterConfig = {
38298
38845
  excludedRepos: new Set(config2.excludeRepos.map((r) => r.toLowerCase())),
38299
38846
  excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
38300
38847
  aiBlocklisted,
38301
38848
  lowScoringRepos,
38302
38849
  skippedUrls: options.skippedUrls ?? /* @__PURE__ */ new Set(),
38303
- maxAgeDays: config2.maxIssueAgeDays || 90,
38304
38850
  now: /* @__PURE__ */ new Date(),
38305
38851
  includeDocIssues: config2.includeDocIssues ?? true
38852
+ };
38853
+ const filterIssues = buildIssueFilter({
38854
+ ...baseFilterConfig,
38855
+ maxAgeDays: config2.maxIssueAgeDays || 90
38856
+ });
38857
+ const filterIssuesPhase0 = buildIssueFilter({
38858
+ ...baseFilterConfig,
38859
+ maxAgeDays: CONTRIBUTED_REPO_MAX_AGE_DAYS
38306
38860
  });
38307
38861
  const seenPhase0 = /* @__PURE__ */ new Set();
38308
38862
  const phase0Repos = [];
@@ -38315,10 +38869,12 @@ var IssueDiscovery = class {
38315
38869
  break;
38316
38870
  }
38317
38871
  const phase0RepoSet = new Set(phase0Repos);
38872
+ const otherStrategiesCanRun = starredRepos.length > 0 && enabledStrategies.has("starred") || enabledStrategies.has("broad") || enabledStrategies.has("maintained");
38318
38873
  if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
38319
- const remaining = maxResults - allCandidates.length;
38874
+ const phase0Cap = otherStrategiesCanRun ? Math.max(1, Math.ceil(maxResults * PHASE0_MAX_SHARE)) : maxResults;
38875
+ const remaining = Math.min(maxResults - allCandidates.length, phase0Cap);
38320
38876
  if (remaining > 0) {
38321
- const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
38877
+ const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssuesPhase0);
38322
38878
  recordPhaseResult("0", result);
38323
38879
  }
38324
38880
  strategiesUsed.push("merged");
@@ -38338,37 +38894,39 @@ var IssueDiscovery = class {
38338
38894
  const broadDelay = options.broadPhaseDelayMs ?? config2.broadPhaseDelayMs ?? 9e4;
38339
38895
  const configuredSkipThreshold = config2.skipBroadWhenSufficientResults ?? 8;
38340
38896
  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})`);
38897
+ const viableBeforeBroad = viableCandidateCount();
38898
+ if (viableBeforeBroad >= maxResults && enabledStrategies.has("broad")) {
38899
+ info(MODULE12, `Skipping broad search: ${viableBeforeBroad} viable candidate(s) already meet maxResults (${maxResults})`);
38900
+ } else if (enabledStrategies.has("broad")) {
38901
+ const newRepoCandidateCount = allCandidates.filter((c) => c.recommendation !== "skip" && !phase0RepoSet.has(c.issue.repo) && !starredRepoSet.has(c.issue.repo)).length;
38902
+ if (skipThreshold > 0 && newRepoCandidateCount >= skipThreshold) {
38903
+ info(MODULE12, `Skipping broad search: already found ${newRepoCandidateCount} viable candidate(s) from new repos (threshold: ${skipThreshold})`);
38344
38904
  } else {
38345
38905
  await applyInterPhaseDelay();
38346
38906
  if (allCandidates.length > 0 && broadDelay > 0) {
38347
- info(MODULE11, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
38907
+ info(MODULE12, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
38348
38908
  await sleep(broadDelay);
38349
38909
  } else if (allCandidates.length === 0) {
38350
- info(MODULE11, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
38910
+ info(MODULE12, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
38351
38911
  }
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);
38912
+ const remaining = maxResults - viableBeforeBroad;
38913
+ 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
38914
  recordPhaseResult("2", result);
38355
38915
  strategiesUsed.push("broad");
38356
38916
  }
38357
38917
  }
38358
- if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
38918
+ const viableBeforeMaintained = viableCandidateCount();
38919
+ if (viableBeforeMaintained >= maxResults && enabledStrategies.has("maintained")) {
38920
+ info(MODULE12, `Skipping maintained search: ${viableBeforeMaintained} viable candidate(s) already meet maxResults (${maxResults})`);
38921
+ } else if (enabledStrategies.has("maintained")) {
38359
38922
  await applyInterPhaseDelay();
38360
- const remaining = maxResults - allCandidates.length;
38923
+ const remaining = maxResults - viableBeforeMaintained;
38361
38924
  const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues, tracker);
38362
38925
  recordPhaseResult("3", result);
38363
38926
  strategiesUsed.push("maintained");
38364
38927
  }
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
- }
38928
+ const phasesSkippedForBudget = searchBudget < CRITICAL_BUDGET_THRESHOLD;
38929
+ const budgetNote = phasesSkippedForBudget ? ` The starred-repo phase was skipped due to critically low API quota (${searchBudget} remaining); broad and maintained phases still ran on GraphQL.` : "";
38372
38930
  if (allCandidates.length === 0) {
38373
38931
  const errorDetails = [
38374
38932
  phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
@@ -38381,7 +38939,7 @@ var IssueDiscovery = class {
38381
38939
  this.rateLimitWarning = `Search returned no results due to GitHub API rate limits.${details}${budgetNote} Try again after the rate limit resets.`;
38382
38940
  return { candidates: [], strategiesUsed };
38383
38941
  }
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.`);
38942
+ 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
38943
  }
38386
38944
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
38387
38945
  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 +38968,13 @@ var IssueDiscovery = class {
38410
38968
  return boostDiff;
38411
38969
  return b.viabilityScore - a.viabilityScore;
38412
38970
  });
38413
- const capped = applyPerRepoCap(ranked, 2);
38971
+ const viableFirst = [
38972
+ ...ranked.filter((c) => c.recommendation !== "skip"),
38973
+ ...ranked.filter((c) => c.recommendation === "skip")
38974
+ ];
38975
+ const capped = applyPerRepoCap(viableFirst, 2);
38414
38976
  const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
38415
- info(MODULE11, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${finalPicks.length} candidates returned`);
38977
+ info(MODULE12, `Search complete: ${tracker.getTotalCalls()} REST Search API call(s) + ${getGraphQLSearchQueryCount()} GraphQL broad-search query/queries used, ${finalPicks.length} candidates returned`);
38416
38978
  return { candidates: finalPicks, strategiesUsed };
38417
38979
  }
38418
38980
  /**
@@ -38464,8 +39026,8 @@ function pruneCache2() {
38464
39026
  function parseRoadmapIssueRefs(content, owner, repo) {
38465
39027
  const refs = /* @__PURE__ */ new Set();
38466
39028
  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");
39029
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39030
+ const fullRefPattern = new RegExp(`\\b${escape(owner)}/${escape(repo)}#(\\d+)\\b`, "gi");
38469
39031
  for (const m of content.matchAll(fullRefPattern)) {
38470
39032
  const n = Number.parseInt(m[1], 10);
38471
39033
  if (n > 0)
@@ -38527,7 +39089,7 @@ async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
38527
39089
  }
38528
39090
 
38529
39091
  // ../core/dist/core/feature-discovery.js
38530
- var MODULE12 = "feature-discovery";
39092
+ var MODULE13 = "feature-discovery";
38531
39093
  var INTER_REPO_DELAY_MS = 2e3;
38532
39094
  var MIN_VIABILITY_SCORE = 40;
38533
39095
  var ANCHOR_THRESHOLD = 3;
@@ -38640,7 +39202,7 @@ async function vetAndClassify(item, vetter, roadmapRefs) {
38640
39202
  });
38641
39203
  } catch (err) {
38642
39204
  rethrowIfFatal(err);
38643
- warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
39205
+ warn(MODULE13, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
38644
39206
  return null;
38645
39207
  }
38646
39208
  const horizon = classifyHorizon({
@@ -38683,7 +39245,7 @@ async function discoverFeatures(opts) {
38683
39245
  roadmapRefs = refs;
38684
39246
  } catch (err) {
38685
39247
  rethrowIfFatal(err);
38686
- warn(MODULE12, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
39248
+ warn(MODULE13, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
38687
39249
  continue;
38688
39250
  }
38689
39251
  const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
@@ -38761,7 +39323,7 @@ async function discoverFeaturesBroad(opts) {
38761
39323
  items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).slice(0, maxToVet);
38762
39324
  } catch (err) {
38763
39325
  rethrowIfFatal(err);
38764
- warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
39326
+ warn(MODULE13, `broad feature search failed: ${errorMessage(err)}`);
38765
39327
  return {
38766
39328
  quickWins: [],
38767
39329
  biggerBets: [],
@@ -38788,7 +39350,7 @@ async function discoverFeaturesBroad(opts) {
38788
39350
  // ../core/dist/core/gist-state-store.js
38789
39351
  var fs3 = __toESM(require("fs"), 1);
38790
39352
  var path3 = __toESM(require("path"), 1);
38791
- var MODULE13 = "gist-state";
39353
+ var MODULE14 = "gist-state";
38792
39354
  var GIST_DESCRIPTION = "oss-scout-state";
38793
39355
  var GIST_FILENAME = "state.json";
38794
39356
  var GIST_ID_FILE = "gist-id";
@@ -38833,7 +39395,7 @@ var GistStateStore = class {
38833
39395
  } catch (err) {
38834
39396
  if (getHttpStatusCode(err) === 401)
38835
39397
  throw err;
38836
- warn(MODULE13, `API bootstrap failed: ${errorMessage(err)}`);
39398
+ warn(MODULE14, `API bootstrap failed: ${errorMessage(err)}`);
38837
39399
  return this.bootstrapFromCache(classifyDegradedReason(err));
38838
39400
  }
38839
39401
  }
@@ -38842,24 +39404,28 @@ var GistStateStore = class {
38842
39404
  */
38843
39405
  async push(state) {
38844
39406
  if (!this.gistId) {
38845
- warn(MODULE13, "No gist ID \u2014 cannot push");
39407
+ warn(MODULE14, "No gist ID \u2014 cannot push");
38846
39408
  this.writeCache(state);
38847
39409
  return false;
38848
39410
  }
38849
39411
  let toWrite = state;
38850
39412
  try {
38851
39413
  const remote = await this.fetchGistState(this.gistId);
38852
- if (remote) {
38853
- toWrite = mergeStates(state, remote);
39414
+ if (remote.kind === "ok") {
39415
+ toWrite = mergeStates(state, remote.state);
39416
+ } else if (remote.kind === "invalid") {
39417
+ this.writeCache(state);
39418
+ warn(MODULE14, "Remote gist content failed validation \u2014 not overwriting it. Changes saved locally; gist sync skipped.");
39419
+ return false;
38854
39420
  }
38855
39421
  } catch (err) {
38856
39422
  rethrowIfFatal(err);
38857
- warn(MODULE13, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
39423
+ warn(MODULE14, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
38858
39424
  }
38859
39425
  this.writeCache(toWrite);
38860
39426
  const json2 = JSON.stringify(toWrite, null, 2);
38861
39427
  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'.`);
39428
+ warn(MODULE14, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
38863
39429
  return false;
38864
39430
  }
38865
39431
  try {
@@ -38869,11 +39435,11 @@ var GistStateStore = class {
38869
39435
  [GIST_FILENAME]: { content: json2 }
38870
39436
  }
38871
39437
  });
38872
- debug(MODULE13, "State pushed to gist");
39438
+ debug(MODULE14, "State pushed to gist");
38873
39439
  return true;
38874
39440
  } catch (err) {
38875
39441
  rethrowIfFatal(err);
38876
- warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
39442
+ warn(MODULE14, `Failed to push: ${errorMessage(err)}`);
38877
39443
  return false;
38878
39444
  }
38879
39445
  }
@@ -38885,40 +39451,40 @@ var GistStateStore = class {
38885
39451
  async bootstrapFromApi() {
38886
39452
  const cachedId = this.readCachedGistId();
38887
39453
  if (cachedId) {
38888
- debug(MODULE13, `Trying cached gist ID: ${cachedId}`);
39454
+ debug(MODULE14, `Trying cached gist ID: ${cachedId}`);
38889
39455
  try {
38890
39456
  const fetched = await this.fetchGistState(cachedId);
38891
- if (fetched) {
39457
+ if (fetched.kind === "ok") {
38892
39458
  this.gistId = cachedId;
38893
- const state = this.mergeCacheInto(fetched);
39459
+ const state = this.mergeCacheInto(fetched.state);
38894
39460
  this.writeCache(state);
38895
39461
  return { gistId: cachedId, state, created: false };
38896
39462
  }
38897
39463
  } catch (err) {
38898
39464
  if (getHttpStatusCode(err) !== 404)
38899
39465
  throw err;
38900
- debug(MODULE13, `Cached gist gone (404): ${errorMessage(err)}`);
39466
+ debug(MODULE14, `Cached gist gone (404): ${errorMessage(err)}`);
38901
39467
  }
38902
- debug(MODULE13, "Cached gist ID invalid, searching...");
39468
+ debug(MODULE14, "Cached gist ID invalid, searching...");
38903
39469
  }
38904
39470
  const search = await this.searchForGist();
38905
39471
  if (search.id) {
38906
- debug(MODULE13, `Found gist via search: ${search.id}`);
39472
+ debug(MODULE14, `Found gist via search: ${search.id}`);
38907
39473
  this.saveGistId(search.id);
38908
39474
  this.gistId = search.id;
38909
39475
  const fetched = await this.fetchGistState(search.id);
38910
- if (fetched) {
38911
- const state = this.mergeCacheInto(fetched);
39476
+ if (fetched.kind === "ok") {
39477
+ const state = this.mergeCacheInto(fetched.state);
38912
39478
  this.writeCache(state);
38913
39479
  return { gistId: search.id, state, created: false };
38914
39480
  }
38915
- warn(MODULE13, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
39481
+ warn(MODULE14, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
38916
39482
  return this.bootstrapFromCache("unknown");
38917
39483
  }
38918
39484
  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.`);
39485
+ 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
39486
  }
38921
- debug(MODULE13, "No existing gist found, creating new one");
39487
+ debug(MODULE14, "No existing gist found, creating new one");
38922
39488
  const freshState = ScoutStateSchema.parse({ version: 1 });
38923
39489
  const newId = await this.createGist(freshState);
38924
39490
  this.saveGistId(newId);
@@ -38929,7 +39495,7 @@ var GistStateStore = class {
38929
39495
  bootstrapFromCache(reason) {
38930
39496
  const cached2 = this.readCache();
38931
39497
  if (cached2) {
38932
- debug(MODULE13, "Bootstrapped from local cache (degraded mode)");
39498
+ debug(MODULE14, "Bootstrapped from local cache (degraded mode)");
38933
39499
  const cachedId = this.readCachedGistId();
38934
39500
  if (cachedId)
38935
39501
  this.gistId = cachedId;
@@ -38941,7 +39507,7 @@ var GistStateStore = class {
38941
39507
  degradedReason: reason
38942
39508
  };
38943
39509
  }
38944
- debug(MODULE13, "No cache available, using fresh state (degraded mode)");
39510
+ debug(MODULE14, "No cache available, using fresh state (degraded mode)");
38945
39511
  const fresh = ScoutStateSchema.parse({ version: 1 });
38946
39512
  return {
38947
39513
  gistId: "",
@@ -38952,17 +39518,24 @@ var GistStateStore = class {
38952
39518
  };
38953
39519
  }
38954
39520
  // ── Gist API operations ──────────────────────────────────────────────
39521
+ /**
39522
+ * "missing" (no file/content) and "invalid" (content that fails parse or
39523
+ * schema validation) are distinct outcomes: an invalid remote may be a
39524
+ * corrupted-but-recoverable state or a newer state version written by a
39525
+ * newer binary, so callers must not treat it as "nothing there" and
39526
+ * overwrite it (#286).
39527
+ */
38955
39528
  async fetchGistState(gistId) {
38956
39529
  const { data } = await this.octokit.gists.get({ gist_id: gistId });
38957
39530
  const file2 = data.files?.[GIST_FILENAME];
38958
39531
  if (!file2?.content)
38959
- return null;
39532
+ return { kind: "missing" };
38960
39533
  try {
38961
39534
  const parsed = JSON.parse(file2.content);
38962
- return parseScoutState(parsed);
39535
+ return { kind: "ok", state: parseScoutState(parsed) };
38963
39536
  } catch (err) {
38964
- warn(MODULE13, `Gist content failed validation: ${errorMessage(err)}`);
38965
- return null;
39537
+ warn(MODULE14, `Gist content failed validation: ${errorMessage(err)}`);
39538
+ return { kind: "invalid" };
38966
39539
  }
38967
39540
  }
38968
39541
  /**
@@ -39004,7 +39577,7 @@ var GistStateStore = class {
39004
39577
  } catch (err) {
39005
39578
  const code = err?.code;
39006
39579
  if (code !== "ENOENT") {
39007
- warn(MODULE13, `Failed to read cached gist ID: ${errorMessage(err)}`);
39580
+ warn(MODULE14, `Failed to read cached gist ID: ${errorMessage(err)}`);
39008
39581
  }
39009
39582
  return null;
39010
39583
  }
@@ -39030,7 +39603,7 @@ var GistStateStore = class {
39030
39603
  } catch (err) {
39031
39604
  const code = err?.code;
39032
39605
  if (code !== "ENOENT") {
39033
- warn(MODULE13, `Failed to read state cache: ${errorMessage(err)}`);
39606
+ warn(MODULE14, `Failed to read state cache: ${errorMessage(err)}`);
39034
39607
  }
39035
39608
  return null;
39036
39609
  }
@@ -39041,7 +39614,7 @@ var GistStateStore = class {
39041
39614
  mode: 384
39042
39615
  });
39043
39616
  } catch (err) {
39044
- warn(MODULE13, `Failed to write cache: ${errorMessage(err)}`);
39617
+ warn(MODULE14, `Failed to write cache: ${errorMessage(err)}`);
39045
39618
  }
39046
39619
  }
39047
39620
  };
@@ -39056,7 +39629,7 @@ function mergeTombstones(local, remote) {
39056
39629
  }
39057
39630
  return [...byUrl.values()].filter((t) => {
39058
39631
  const ts = new Date(t.removedAt).getTime();
39059
- return !Number.isFinite(ts) || ts >= cutoff;
39632
+ return Number.isFinite(ts) && ts >= cutoff;
39060
39633
  });
39061
39634
  }
39062
39635
  function applyTombstones(items, tombstones, urlOf, touchedAtOf) {
@@ -39098,11 +39671,26 @@ function mergeStates(local, remote) {
39098
39671
  skippedIssues,
39099
39672
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
39100
39673
  lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
39101
- gistId: remote.gistId ?? local.gistId
39674
+ gistId: remote.gistId ?? local.gistId,
39675
+ searchRotation: mergeSearchRotation(local.searchRotation, remote.searchRotation)
39102
39676
  };
39103
39677
  }
39678
+ function mergeSearchRotation(local, remote) {
39679
+ const localRotation = local ?? { languageOffset: 0 };
39680
+ const remoteRotation = remote ?? { languageOffset: 0 };
39681
+ const localTs = localRotation.lastRotatedAt;
39682
+ const remoteTs = remoteRotation.lastRotatedAt;
39683
+ if (localTs && remoteTs) {
39684
+ return localTs >= remoteTs ? localRotation : remoteRotation;
39685
+ }
39686
+ if (localTs)
39687
+ return localRotation;
39688
+ if (remoteTs)
39689
+ return remoteRotation;
39690
+ return remoteRotation.languageOffset >= localRotation.languageOffset ? remoteRotation : localRotation;
39691
+ }
39104
39692
  function mergeRepoScores(local, remote) {
39105
- const merged = { ...local };
39693
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), local);
39106
39694
  for (const [repo, remoteScore] of Object.entries(remote)) {
39107
39695
  const localScore = merged[repo];
39108
39696
  if (!localScore) {
@@ -39169,7 +39757,7 @@ function pickFresherTimestamp(a, b) {
39169
39757
  // ../core/dist/core/local-state.js
39170
39758
  var fs4 = __toESM(require("fs"), 1);
39171
39759
  var path4 = __toESM(require("path"), 1);
39172
- var MODULE14 = "local-state";
39760
+ var MODULE15 = "local-state";
39173
39761
  function getStatePath() {
39174
39762
  return path4.join(getDataDir(), "state.json");
39175
39763
  }
@@ -39183,13 +39771,13 @@ function loadLocalState() {
39183
39771
  if (code === "ENOENT") {
39184
39772
  return ScoutStateSchema.parse({ version: 1 });
39185
39773
  }
39186
- warn(MODULE14, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
39774
+ warn(MODULE15, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
39187
39775
  try {
39188
39776
  const backupPath = `${statePath}.corrupt.${Date.now()}`;
39189
39777
  fs4.copyFileSync(statePath, backupPath);
39190
- warn(MODULE14, `Corrupt state backed up to ${backupPath}`);
39778
+ warn(MODULE15, `Corrupt state backed up to ${backupPath}`);
39191
39779
  } catch (backupErr) {
39192
- warn(MODULE14, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
39780
+ warn(MODULE15, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
39193
39781
  }
39194
39782
  return ScoutStateSchema.parse({ version: 1 });
39195
39783
  }
@@ -39209,7 +39797,7 @@ function saveLocalState(state) {
39209
39797
  }
39210
39798
  throw err;
39211
39799
  }
39212
- debug(MODULE14, "State saved");
39800
+ debug(MODULE15, "State saved");
39213
39801
  }
39214
39802
 
39215
39803
  // ../core/dist/scout.js
@@ -39322,6 +39910,15 @@ var OssScout = class {
39322
39910
  this.evictStaleCacheEntries();
39323
39911
  this.cullExpiredSkips();
39324
39912
  const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
39913
+ if (options?.excludeRecentlySurfaced ?? true) {
39914
+ const ttlDays = options?.recentlySurfacedTtlDays ?? 7;
39915
+ const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1e3;
39916
+ for (const r of this.state.savedResults ?? []) {
39917
+ const seen = Date.parse(r.lastSeenAt);
39918
+ if (!Number.isNaN(seen) && seen >= cutoff)
39919
+ skippedUrls.add(r.issueUrl);
39920
+ }
39921
+ }
39325
39922
  const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
39326
39923
  const prefs = this.state.preferences;
39327
39924
  const prefLangs = prefs.preferLanguages ?? [];
@@ -39333,18 +39930,30 @@ var OssScout = class {
39333
39930
  const avoidRepos = options?.avoidRepos ?? (prefAvoid.length > 0 ? prefAvoid : void 0);
39334
39931
  const boostIssueTypes = options?.boostIssueTypes ?? (prefBoostTypes.length > 0 ? prefBoostTypes : void 0);
39335
39932
  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
- });
39933
+ const rotation = this.state.searchRotation ?? { languageOffset: 0 };
39934
+ let candidates;
39935
+ let strategiesUsed;
39936
+ try {
39937
+ ({ candidates, strategiesUsed } = await discovery.searchIssues({
39938
+ maxResults: options?.maxResults,
39939
+ strategies: options?.strategies,
39940
+ skippedUrls,
39941
+ preferLanguages,
39942
+ preferRepos,
39943
+ avoidRepos,
39944
+ boostIssueTypes,
39945
+ diversityRatio,
39946
+ interPhaseDelayMs: options?.interPhaseDelayMs,
39947
+ broadPhaseDelayMs: options?.broadPhaseDelayMs,
39948
+ languageRotationOffset: rotation.languageOffset
39949
+ }));
39950
+ } catch (err) {
39951
+ if (err instanceof ValidationError) {
39952
+ this.advanceRotationIfBroadRan(rotation, err.strategiesUsed ?? []);
39953
+ }
39954
+ throw err;
39955
+ }
39956
+ this.advanceRotationIfBroadRan(rotation, strategiesUsed);
39348
39957
  for (const c of candidates) {
39349
39958
  this.updateRepoSignalsFromHealth(c.projectHealth);
39350
39959
  }
@@ -39358,6 +39967,23 @@ var OssScout = class {
39358
39967
  strategiesUsed
39359
39968
  };
39360
39969
  }
39970
+ /**
39971
+ * Advance the language-rotation cursor when the broad phase actually ran.
39972
+ * Called on both the success path and the zero-candidate ValidationError
39973
+ * path — a broad phase that ran and found nothing must still rotate, or the
39974
+ * same barren language slice would lead every subsequent run (#249
39975
+ * follow-up). Skipped/gated broad phases don't burn a rotation slot.
39976
+ */
39977
+ advanceRotationIfBroadRan(rotation, strategiesUsed) {
39978
+ if (!strategiesUsed.includes("broad"))
39979
+ return;
39980
+ this.state.searchRotation = {
39981
+ ...rotation,
39982
+ languageOffset: rotation.languageOffset + 1,
39983
+ lastRotatedAt: (/* @__PURE__ */ new Date()).toISOString()
39984
+ };
39985
+ this.dirty = true;
39986
+ }
39361
39987
  /**
39362
39988
  * Populate the `hasActiveMaintainers` repo-score signal from a freshly
39363
39989
  * computed projectHealth (#167). It was initialized false and never set, so
@@ -39902,6 +40528,7 @@ var OssScout = class {
39902
40528
  }
39903
40529
  if (this.persistLocal) {
39904
40530
  try {
40531
+ this.state = mergeStates(this.state, loadLocalState());
39905
40532
  saveLocalState(this.state);
39906
40533
  } catch {
39907
40534
  return false;
@@ -40038,6 +40665,8 @@ function applyPreferenceField(preferences, key, value) {
40038
40665
  const prefs = { ...preferences };
40039
40666
  switch (field.type) {
40040
40667
  case "string":
40668
+ if (key === "slmTriageHost")
40669
+ assertValidTriageHost(value);
40041
40670
  prefs[key] = value;
40042
40671
  break;
40043
40672
  case "boolean":
@@ -40117,7 +40746,7 @@ function registerTools(server, scout) {
40117
40746
  "search",
40118
40747
  "Search for open source issues matching your preferences",
40119
40748
  {
40120
- maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
40749
+ maxResults: external_exports3.number().int().min(1).max(50).optional().describe("Maximum number of results to return (1-50, default 10)"),
40121
40750
  strategies: external_exports3.string().optional().describe(
40122
40751
  "Comma-separated search strategies: merged, starred, broad, maintained, all"
40123
40752
  ),
@@ -40147,7 +40776,16 @@ function registerTools(server, scout) {
40147
40776
  diversityRatio
40148
40777
  }) => {
40149
40778
  try {
40150
- const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
40779
+ const parsedStrategies = strategies ? strategies.split(",").map((s) => {
40780
+ const trimmed = s.trim();
40781
+ const parsed = SearchStrategySchema.safeParse(trimmed);
40782
+ if (!parsed.success) {
40783
+ throw new Error(
40784
+ `Invalid strategy "${trimmed}". Valid strategies: ${SearchStrategySchema.options.join(", ")}`
40785
+ );
40786
+ }
40787
+ return parsed.data;
40788
+ }) : void 0;
40151
40789
  const result = await withTimeout(
40152
40790
  scout.search({
40153
40791
  maxResults: maxResults ?? 10,
@@ -40190,7 +40828,7 @@ function registerTools(server, scout) {
40190
40828
  "scout-features",
40191
40829
  "Surface feature-scoped contribution opportunities in repos where you have 3+ merged PRs",
40192
40830
  {
40193
- maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
40831
+ maxResults: external_exports3.number().int().min(1).max(50).optional().describe("Maximum number of results to return (1-50, default 10)"),
40194
40832
  anchorThreshold: external_exports3.number().int().min(1).max(50).optional().describe("Anchor threshold override (default 3)"),
40195
40833
  splitRatio: external_exports3.number().min(0).max(1).optional().describe("Quick-wins/bigger-bets split ratio (default 0.6)")
40196
40834
  },
@@ -40457,7 +41095,7 @@ function registerResources(server, scout) {
40457
41095
  // src/index.ts
40458
41096
  var SERVER_INFO = {
40459
41097
  name: "oss-scout-mcp",
40460
- version: "0.10.0"
41098
+ version: "0.10.1"
40461
41099
  // x-release-please-version
40462
41100
  };
40463
41101
  function createServer(scout) {