@oss-scout/mcp 0.9.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;
3376
+ } else {
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;
3355
3417
  } else {
3356
- output += escape(input[i]);
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;
@@ -35867,6 +36088,9 @@ var CONCRETE_STRATEGIES = [
35867
36088
  ];
35868
36089
  var RepoSignalsSchema = external_exports3.looseObject({
35869
36090
  hasActiveMaintainers: external_exports3.boolean(),
36091
+ // Retained for backward compatibility but no longer affects the repo score
36092
+ // (#167): nothing computes it, and hasActiveMaintainers is the live activity
36093
+ // proxy. Kept so old persisted state and the search JSON output still parse.
35870
36094
  isResponsive: external_exports3.boolean(),
35871
36095
  hasHostileComments: external_exports3.boolean()
35872
36096
  });
@@ -36009,7 +36233,17 @@ var ScoutPreferencesSchema = external_exports3.looseObject({
36009
36233
  preferLanguages: external_exports3.array(external_exports3.string()).default([]),
36010
36234
  preferRepos: external_exports3.array(external_exports3.string()).default([]),
36011
36235
  diversityRatio: external_exports3.number().min(0).max(1).default(0),
36012
- broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
36236
+ // Soft penalty (milder than the hard excludeRepos filter): candidates in
36237
+ // these `owner/repo` slugs are pushed down the ranking but not removed (#168).
36238
+ avoidRepos: external_exports3.array(external_exports3.string()).default([]),
36239
+ // Soft boost for candidates whose issue labels match one of these types,
36240
+ // case-insensitive (e.g. "bug", "good first issue") (#168).
36241
+ boostIssueTypes: external_exports3.array(external_exports3.string()).default([]),
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),
36013
36247
  /**
36014
36248
  * Skip the expensive broad phase once this many candidates were found by
36015
36249
  * the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
@@ -36072,7 +36306,20 @@ var ScoutStateSchema = external_exports3.looseObject({
36072
36306
  preferencesUpdatedAt: external_exports3.string().optional(),
36073
36307
  lastSearchAt: external_exports3.string().optional(),
36074
36308
  lastRunAt: external_exports3.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
36075
- 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 }))
36076
36323
  });
36077
36324
  function parseScoutState(raw) {
36078
36325
  return ScoutStateSchema.parse(raw);
@@ -36725,16 +36972,18 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
36725
36972
  }
36726
36973
  }
36727
36974
  var MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1e3;
36728
- async function checkUserMergedPRsInRepo(octokit, owner, repo) {
36975
+ function mergedPRsCacheKey(owner, repo) {
36976
+ return versionedCacheKey(`merged-prs:${owner}/${repo}`);
36977
+ }
36978
+ async function checkUserMergedPRsInRepo(octokit, owner, repo, tracker = getSearchBudgetTracker()) {
36729
36979
  const cache = getHttpCache();
36730
- const cacheKey = versionedCacheKey(`merged-prs:${owner}/${repo}`);
36980
+ const cacheKey = mergedPRsCacheKey(owner, repo);
36731
36981
  return withInflightDedup(cache, cacheKey, async () => {
36732
36982
  const cached2 = cache.getIfFresh(cacheKey, MERGED_PR_CACHE_TTL_MS);
36733
36983
  if (cached2 != null && typeof cached2 === "number") {
36734
36984
  return cached2;
36735
36985
  }
36736
36986
  try {
36737
- const tracker = getSearchBudgetTracker();
36738
36987
  await tracker.waitForBudget();
36739
36988
  try {
36740
36989
  const { data } = await octokit.search.issuesAndPullRequests({
@@ -36773,6 +37022,17 @@ async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount)
36773
37022
  });
36774
37023
  recentComments.push(...response.data);
36775
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
+ }
36776
37036
  for (const comment of recentComments) {
36777
37037
  if (commentClaimsIssue(comment.body || "")) {
36778
37038
  return { passed: false };
@@ -36801,8 +37061,54 @@ function analyzeRequirements(body) {
36801
37061
  return indicators.filter(Boolean).length >= 2;
36802
37062
  }
36803
37063
 
37064
+ // ../core/dist/core/probe-repo-file.js
37065
+ var MODULE6 = "probe-repo-file";
37066
+ async function probeRepoFile(octokit, owner, repo, path5) {
37067
+ try {
37068
+ const data = await cachedRequest(getHttpCache(), `/repos/${owner}/${repo}/contents/${path5}`, (headers) => octokit.repos.getContent({ owner, repo, path: path5, headers }));
37069
+ if (data && typeof data === "object" && "content" in data && typeof data.content === "string") {
37070
+ return {
37071
+ text: Buffer.from(data.content, "base64").toString("utf-8"),
37072
+ transient: false
37073
+ };
37074
+ }
37075
+ return { text: null, transient: false };
37076
+ } catch (error48) {
37077
+ const status = getHttpStatusCode(error48);
37078
+ if (status === 404)
37079
+ return { text: null, transient: false };
37080
+ rethrowIfFatal(error48);
37081
+ warn(MODULE6, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
37082
+ return { text: null, transient: true };
37083
+ }
37084
+ }
37085
+
36804
37086
  // ../core/dist/core/repo-health.js
36805
- var MODULE6 = "repo-health";
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
+ }
36806
37112
  var guidelinesCache = /* @__PURE__ */ new Map();
36807
37113
  var CACHE_TTL_MS = 60 * 60 * 1e3;
36808
37114
  var HEALTH_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
@@ -36822,22 +37128,25 @@ function pruneCache() {
36822
37128
  }
36823
37129
  }
36824
37130
  }
36825
- async function checkProjectHealth(octokit, owner, repo) {
37131
+ async function checkProjectHealth(octokit, owner, repo, tracker = getSearchBudgetTracker()) {
36826
37132
  const cache = getHttpCache();
36827
- const healthCacheKey = `health:${owner}/${repo}`;
37133
+ const healthCacheKey = versionedCacheKey(`health:${owner}/${repo}`);
36828
37134
  try {
36829
37135
  return await cachedTimeBased(cache, healthCacheKey, HEALTH_CACHE_TTL_MS, async () => {
36830
37136
  const url2 = `/repos/${owner}/${repo}`;
36831
37137
  const repoData = await cachedRequest(cache, url2, (headers) => octokit.repos.get({ owner, repo, headers }));
36832
- 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({
36833
37140
  owner,
36834
37141
  repo,
36835
- per_page: 1
36836
- });
37142
+ per_page: 1,
37143
+ headers
37144
+ }));
36837
37145
  const lastCommit = commits[0];
36838
37146
  const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
36839
37147
  const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
36840
37148
  const ciStatus = "unknown";
37149
+ const mergeStats = await fetchRepoMergeStats(octokit, owner, repo, tracker);
36841
37150
  return {
36842
37151
  repo: `${owner}/${repo}`,
36843
37152
  lastCommitAt,
@@ -36849,13 +37158,15 @@ async function checkProjectHealth(octokit, owner, repo) {
36849
37158
  isActive: daysSinceLastCommit < 30,
36850
37159
  stargazersCount: repoData.stargazers_count,
36851
37160
  forksCount: repoData.forks_count,
36852
- language: repoData.language
37161
+ language: repoData.language,
37162
+ recentMergedPRCount: mergeStats.recentMergedPRCount,
37163
+ recentMergeRate: mergeStats.recentMergeRate
36853
37164
  };
36854
37165
  });
36855
37166
  } catch (error48) {
36856
37167
  rethrowIfFatal(error48);
36857
37168
  const errMsg = errorMessage(error48);
36858
- warn(MODULE6, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
37169
+ warn(MODULE7, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
36859
37170
  return {
36860
37171
  repo: `${owner}/${repo}`,
36861
37172
  checkFailed: true,
@@ -36889,12 +37200,7 @@ async function fetchContributionGuidelinesUncached(octokit, owner, repo) {
36889
37200
  "docs/CONTRIBUTING.md",
36890
37201
  "contributing.md"
36891
37202
  ];
36892
- const results = await Promise.allSettled(filesToCheck.map((file2) => octokit.repos.getContent({ owner, repo, path: file2 }).then(({ data }) => {
36893
- if ("content" in data) {
36894
- return Buffer.from(data.content, "base64").toString("utf-8");
36895
- }
36896
- return null;
36897
- })));
37203
+ const results = await Promise.allSettled(filesToCheck.map((file2) => probeRepoFile(octokit, owner, repo, file2)));
36898
37204
  for (const result of results) {
36899
37205
  if (result.status !== "rejected")
36900
37206
  continue;
@@ -36902,20 +37208,13 @@ async function fetchContributionGuidelinesUncached(octokit, owner, repo) {
36902
37208
  throw result.reason;
36903
37209
  }
36904
37210
  }
36905
- for (let i = 0; i < results.length; i++) {
36906
- const result = results[i];
36907
- if (result.status === "fulfilled" && result.value) {
36908
- const guidelines = parseContributionGuidelines(result.value);
37211
+ for (const result of results) {
37212
+ if (result.status === "fulfilled" && result.value.text) {
37213
+ const guidelines = parseContributionGuidelines(result.value.text);
36909
37214
  guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
36910
37215
  pruneCache();
36911
37216
  return guidelines;
36912
37217
  }
36913
- if (result.status === "rejected") {
36914
- const status = getHttpStatusCode(result.reason);
36915
- if (status !== 404) {
36916
- warn(MODULE6, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${errorMessage(result.reason)}`);
36917
- }
36918
- }
36919
37218
  }
36920
37219
  guidelinesCache.set(cacheKey, {
36921
37220
  guidelines: void 0,
@@ -36964,7 +37263,6 @@ function parseContributionGuidelines(content) {
36964
37263
  }
36965
37264
 
36966
37265
  // ../core/dist/core/anti-llm-policy.js
36967
- var MODULE7 = "anti-llm-policy";
36968
37266
  var POLICY_SCAN_CACHE_TTL_MS = 60 * 60 * 1e3;
36969
37267
  var ANTI_LLM_KEYWORDS = [
36970
37268
  "no ai-generated",
@@ -37027,27 +37325,8 @@ var SOURCE_FILE_FAMILIES = [
37027
37325
  paths: ["README.md", "readme.md", "Readme.md"]
37028
37326
  }
37029
37327
  ];
37030
- async function fetchFileText(octokit, owner, repo, path5) {
37031
- try {
37032
- const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
37033
- if ("content" in data && typeof data.content === "string") {
37034
- return {
37035
- text: Buffer.from(data.content, "base64").toString("utf-8"),
37036
- transient: false
37037
- };
37038
- }
37039
- return { text: null, transient: false };
37040
- } catch (error48) {
37041
- const status = getHttpStatusCode(error48);
37042
- if (status === 404)
37043
- return { text: null, transient: false };
37044
- rethrowIfFatal(error48);
37045
- warn(MODULE7, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
37046
- return { text: null, transient: true };
37047
- }
37048
- }
37049
37328
  async function fetchFamilyText(octokit, owner, repo, paths) {
37050
- const results = await Promise.allSettled(paths.map((p) => fetchFileText(octokit, owner, repo, p)));
37329
+ const results = await Promise.allSettled(paths.map((p) => probeRepoFile(octokit, owner, repo, p)));
37051
37330
  let hadTransientFailure = false;
37052
37331
  for (const result of results) {
37053
37332
  if (result.status === "fulfilled") {
@@ -37186,8 +37465,133 @@ ${selections.join("\n")}
37186
37465
  });
37187
37466
  return result;
37188
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
+ }
37189
37592
 
37190
37593
  // ../core/dist/core/slm-triage.js
37594
+ var MODULE9 = "slm-triage";
37191
37595
  var DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
37192
37596
  var DEFAULT_TIMEOUT_MS = 15e3;
37193
37597
  var MAX_BODY_CHARS = 2e3;
@@ -37224,10 +37628,48 @@ function buildPrompt(input) {
37224
37628
  `Linked PR exists: ${linkedPRExists}`
37225
37629
  ].join("\n");
37226
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
+ }
37227
37665
  async function triageWithSLM(input, options) {
37228
37666
  if (!options.model)
37229
37667
  return null;
37230
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
+ }
37231
37673
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
37232
37674
  const fetchFn = options.fetchImpl ?? fetch;
37233
37675
  let response;
@@ -37299,15 +37741,23 @@ function isValidTriageShape(value) {
37299
37741
  }
37300
37742
 
37301
37743
  // ../core/dist/core/issue-vetting.js
37302
- var MODULE9 = "issue-vetting";
37744
+ var MODULE10 = "issue-vetting";
37303
37745
  var MAX_CONCURRENT_VETTING = 3;
37304
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
+ }
37305
37755
  function deriveRecommendation(input) {
37306
37756
  const notes = [];
37307
37757
  const reasonsToApprove = [];
37308
37758
  const reasonsToSkip = [];
37309
37759
  if (!input.noExistingPR)
37310
- 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");
37311
37761
  if (!input.notClaimed)
37312
37762
  notes.push("Issue appears to be claimed by someone");
37313
37763
  if (input.existingPRInconclusive) {
@@ -37325,14 +37775,28 @@ function deriveRecommendation(input) {
37325
37775
  notes.push("Issue requirements are unclear");
37326
37776
  if (!input.contributionGuidelinesFound)
37327
37777
  notes.push("No CONTRIBUTING.md found");
37328
- if (!input.noExistingPR)
37329
- reasonsToSkip.push(input.ownPR ? "You already have a PR in flight" : "Has existing PR");
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
+ }
37330
37792
  if (!input.notClaimed)
37331
37793
  reasonsToSkip.push("Already claimed");
37332
37794
  if (!input.projectIsActive && !input.projectCheckFailed)
37333
37795
  reasonsToSkip.push("Inactive project");
37334
37796
  if (!input.clearRequirements)
37335
37797
  reasonsToSkip.push("Unclear requirements");
37798
+ if (input.repoAcceptsContributions === false)
37799
+ reasonsToSkip.push("Repo has no recent merged PRs");
37336
37800
  if (input.noExistingPR)
37337
37801
  reasonsToApprove.push("No existing PR");
37338
37802
  if (input.notClaimed)
@@ -37358,16 +37822,18 @@ function deriveRecommendation(input) {
37358
37822
  let recommendation;
37359
37823
  if (input.issueClosed) {
37360
37824
  recommendation = "skip";
37825
+ } else if (input.linkedPRMerged || input.linkedPRClosed) {
37826
+ recommendation = "skip";
37361
37827
  } else if (input.ownPR) {
37362
37828
  recommendation = "skip";
37363
- } else if (input.passedAllChecks) {
37829
+ } else if (input.passedAllChecks && input.repoAcceptsContributions !== false) {
37364
37830
  recommendation = "approve";
37365
37831
  } else if (reasonsToSkip.length > 2) {
37366
37832
  recommendation = "skip";
37367
37833
  } else {
37368
37834
  recommendation = "needs_review";
37369
37835
  }
37370
- const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive;
37836
+ const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive || input.repoAcceptsContributions === null;
37371
37837
  if (recommendation === "approve" && hasInconclusiveChecks) {
37372
37838
  recommendation = "needs_review";
37373
37839
  notes.push("Recommendation downgraded: one or more checks were inconclusive");
@@ -37377,9 +37843,18 @@ function deriveRecommendation(input) {
37377
37843
  var IssueVetter = class {
37378
37844
  octokit;
37379
37845
  stateReader;
37380
- constructor(octokit, stateReader) {
37846
+ budgetTracker;
37847
+ /**
37848
+ * @param octokit - Authenticated Octokit instance
37849
+ * @param stateReader - Read-only scout state interface
37850
+ * @param budgetTracker - Search budget tracker. Defaults to the shared
37851
+ * singleton so existing callers behave identically; inject a per-search
37852
+ * instance to isolate budget accounting in a long-lived concurrent host.
37853
+ */
37854
+ constructor(octokit, stateReader, budgetTracker = getSearchBudgetTracker()) {
37381
37855
  this.octokit = octokit;
37382
37856
  this.stateReader = stateReader;
37857
+ this.budgetTracker = budgetTracker;
37383
37858
  }
37384
37859
  /**
37385
37860
  * Vet a specific issue — runs all checks and computes recommendation + viability score.
@@ -37396,7 +37871,7 @@ var IssueVetter = class {
37396
37871
  const cacheKey = versionedCacheKey(`vet:${issueUrl}${sigKey}`);
37397
37872
  const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
37398
37873
  if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
37399
- debug(MODULE9, `Vetting cache hit for ${issueUrl}`);
37874
+ debug(MODULE10, `Vetting cache hit for ${issueUrl}`);
37400
37875
  return cached2;
37401
37876
  }
37402
37877
  const parsed = parseGitHubUrl(issueUrl);
@@ -37411,9 +37886,9 @@ var IssueVetter = class {
37411
37886
  const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
37412
37887
  checkNoExistingPR(this.octokit, owner, repo, number4),
37413
37888
  checkNotClaimed(this.octokit, owner, repo, number4, core.commentCount),
37414
- checkProjectHealth(this.octokit, owner, repo),
37889
+ checkProjectHealth(this.octokit, owner, repo, this.budgetTracker),
37415
37890
  fetchContributionGuidelines(this.octokit, owner, repo),
37416
- hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo)
37891
+ hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo, this.budgetTracker)
37417
37892
  ]);
37418
37893
  const antiLLMPolicy = await fetchAndScanAntiLLMPolicy(this.octokit, owner, repo, { contributingText: contributionGuidelines?.rawContent });
37419
37894
  const noExistingPR = existingPRCheck.passed;
@@ -37421,8 +37896,11 @@ var IssueVetter = class {
37421
37896
  const username = this.stateReader.getGitHubUsername?.() ?? "";
37422
37897
  const linkedPR = existingPRCheck.linkedPR;
37423
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;
37424
37901
  const clearRequirements = analyzeRequirements(core.body);
37425
37902
  const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
37903
+ const repoAcceptsContributions = repoAcceptsContributionsFromHealth(projectHealth);
37426
37904
  const vettingResult = {
37427
37905
  passedAllChecks: noExistingPR && notClaimed && projectActive && clearRequirements,
37428
37906
  checks: {
@@ -37458,10 +37936,12 @@ var IssueVetter = class {
37458
37936
  }
37459
37937
  const matchesCategory = repoBelongsToCategory(repoFullName, this.stateReader.getProjectCategories());
37460
37938
  const issueClosed = core.state === "closed";
37461
- const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive;
37939
+ const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive || repoAcceptsContributions === null;
37462
37940
  const { notes, reasonsToApprove, reasonsToSkip, recommendation } = deriveRecommendation({
37463
37941
  noExistingPR,
37464
37942
  ownPR,
37943
+ linkedPRMerged,
37944
+ linkedPRClosed,
37465
37945
  notClaimed,
37466
37946
  clearRequirements,
37467
37947
  contributionGuidelinesFound: !!contributionGuidelines,
@@ -37478,7 +37958,8 @@ var IssueVetter = class {
37478
37958
  orgHasMergedPRs,
37479
37959
  matchesCategory,
37480
37960
  issueClosed,
37481
- passedAllChecks: vettingResult.passedAllChecks
37961
+ passedAllChecks: vettingResult.passedAllChecks,
37962
+ repoAcceptsContributions
37482
37963
  });
37483
37964
  vettingResult.notes = notes;
37484
37965
  const repoQualityBonus = projectHealth.checkFailed ? 0 : calculateRepoQualityBonus(projectHealth.stargazersCount ?? 0, projectHealth.forksCount ?? 0);
@@ -37558,6 +38039,36 @@ var IssueVetter = class {
37558
38039
  updatedAt: ghIssue.updated_at
37559
38040
  };
37560
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
+ }
37561
38072
  /**
37562
38073
  * Vet multiple issues in parallel with concurrency limit
37563
38074
  */
@@ -37569,10 +38080,14 @@ var IssueVetter = class {
37569
38080
  let attemptedCount = 0;
37570
38081
  let firstAuthError = null;
37571
38082
  const uniqueUrls = [...new Set(urls)];
37572
- const prefetched = await prefetchIssueCores(this.octokit, uniqueUrls.flatMap((url2) => {
38083
+ const issueRefs = uniqueUrls.flatMap((url2) => {
37573
38084
  const p = parseGitHubUrl(url2);
37574
38085
  return p && p.type === "issues" ? [{ owner: p.owner, repo: p.repo, number: p.number }] : [];
37575
- }));
38086
+ });
38087
+ const [prefetched] = await Promise.all([
38088
+ prefetchIssueCores(this.octokit, issueRefs),
38089
+ this.prefetchMergedPRCounts(issueRefs)
38090
+ ]);
37576
38091
  const prefetchFor = (url2) => {
37577
38092
  const p = parseGitHubUrl(url2);
37578
38093
  return p && p.type === "issues" ? prefetched.get(issueCoreKey(p.owner, p.repo, p.number)) : void 0;
@@ -37592,7 +38107,7 @@ var IssueVetter = class {
37592
38107
  candidates.push(candidate);
37593
38108
  }
37594
38109
  }).catch((error48) => {
37595
- if (getHttpStatusCode(error48) === 401) {
38110
+ if (isAuthError(error48)) {
37596
38111
  firstAuthError ??= error48;
37597
38112
  return;
37598
38113
  }
@@ -37600,7 +38115,7 @@ var IssueVetter = class {
37600
38115
  if (isRateLimitError(error48)) {
37601
38116
  rateLimitFailures++;
37602
38117
  }
37603
- warn(MODULE9, `Error vetting issue ${url2}:`, errorMessage(error48));
38118
+ warn(MODULE10, `Error vetting issue ${url2}:`, errorMessage(error48));
37604
38119
  }).finally(() => pending.delete(url2));
37605
38120
  pending.set(url2, task);
37606
38121
  if (pending.size >= MAX_CONCURRENT_VETTING) {
@@ -37610,13 +38125,13 @@ var IssueVetter = class {
37610
38125
  await Promise.allSettled(pending.values());
37611
38126
  if (firstAuthError) {
37612
38127
  if (candidates.length > 0) {
37613
- 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`);
37614
38129
  }
37615
38130
  throw firstAuthError;
37616
38131
  }
37617
38132
  const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
37618
38133
  if (allFailed) {
37619
- 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).`);
37620
38135
  }
37621
38136
  return {
37622
38137
  candidates: candidates.slice(0, maxResults),
@@ -37627,14 +38142,22 @@ var IssueVetter = class {
37627
38142
  };
37628
38143
 
37629
38144
  // ../core/dist/core/search-phases.js
37630
- var MODULE10 = "search-phases";
38145
+ var MODULE11 = "search-phases";
37631
38146
  var GITHUB_MAX_BOOLEAN_OPS = 5;
37632
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
+ }
37633
38156
  function chunkLabels(labels, reservedOps = 0) {
37634
38157
  const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
37635
38158
  if (maxPerChunk < 1) {
37636
38159
  if (labels.length > 0) {
37637
- warn(MODULE10, `Label filtering disabled: ${reservedOps} repo/org ORs exceed GitHub's ${GITHUB_MAX_BOOLEAN_OPS} operator limit. All ${labels.length} label(s) dropped from query.`);
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.`);
37638
38161
  }
37639
38162
  return [[]];
37640
38163
  }
@@ -37644,7 +38167,7 @@ function chunkLabels(labels, reservedOps = 0) {
37644
38167
  for (let i = 0; i < labels.length; i += maxPerChunk) {
37645
38168
  chunks.push(labels.slice(i, i + maxPerChunk));
37646
38169
  }
37647
- 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)`);
37648
38171
  return chunks;
37649
38172
  }
37650
38173
  function buildLabelQuery(labels) {
@@ -37676,15 +38199,14 @@ function interleaveArrays(arrays) {
37676
38199
  return result;
37677
38200
  }
37678
38201
  var SEARCH_CACHE_TTL_MS = 15 * 60 * 1e3;
37679
- async function cachedSearchIssues(octokit, params) {
38202
+ async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTracker()) {
37680
38203
  const cacheKey = versionedCacheKey(`search:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
37681
38204
  const cache = getHttpCache();
37682
38205
  const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
37683
38206
  if (cached2) {
37684
- debug(MODULE10, `Search cache hit for query`);
38207
+ debug(MODULE11, `Search cache hit for query`);
37685
38208
  return cached2;
37686
38209
  }
37687
- const tracker = getSearchBudgetTracker();
37688
38210
  await tracker.waitForBudget();
37689
38211
  let data;
37690
38212
  try {
@@ -37696,10 +38218,32 @@ async function cachedSearchIssues(octokit, params) {
37696
38218
  if (data.items.length > 0) {
37697
38219
  cache.set(cacheKey, "", data);
37698
38220
  } else {
37699
- 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)`);
37700
38222
  }
37701
38223
  return data;
37702
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
+ }
37703
38247
  async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResults) {
37704
38248
  const items = [];
37705
38249
  for (const repoFullName of repos) {
@@ -37744,15 +38288,15 @@ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResul
37744
38288
  if (getHttpStatusCode(error48) === 401)
37745
38289
  throw error48;
37746
38290
  if (isRateLimitError(error48)) {
37747
- warn(MODULE10, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
38291
+ warn(MODULE11, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
37748
38292
  break;
37749
38293
  }
37750
- warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
38294
+ warn(MODULE11, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
37751
38295
  }
37752
38296
  }
37753
38297
  return items;
37754
38298
  }
37755
- async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn) {
38299
+ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn, perPage = 5) {
37756
38300
  const candidates = [];
37757
38301
  let failedRepos = 0;
37758
38302
  let rateLimitFailures = 0;
@@ -37774,7 +38318,7 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
37774
38318
  state: "open",
37775
38319
  sort: "created",
37776
38320
  direction: "desc",
37777
- per_page: 5,
38321
+ per_page: perPage,
37778
38322
  ...label !== void 0 ? { labels: label } : {}
37779
38323
  });
37780
38324
  for (const issue2 of response.data) {
@@ -37809,30 +38353,30 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
37809
38353
  if (isRateLimitError(error48)) {
37810
38354
  rateLimitFailures++;
37811
38355
  }
37812
- warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
38356
+ warn(MODULE11, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
37813
38357
  }
37814
38358
  }
37815
38359
  const allReposFailed = failedRepos === repos.length && repos.length > 0;
37816
38360
  const rateLimitHit = rateLimitFailures > 0;
37817
38361
  if (allReposFailed) {
37818
- 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).`);
37819
38363
  }
37820
38364
  return { candidates, allReposFailed, rateLimitHit };
37821
38365
  }
37822
- async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage) {
38366
+ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage, tracker = getSearchBudgetTracker()) {
37823
38367
  const labelChunks = chunkLabels(labels, reservedOps);
37824
38368
  const seenUrls = /* @__PURE__ */ new Set();
37825
38369
  const allItems = [];
37826
38370
  for (let i = 0; i < labelChunks.length; i++) {
37827
38371
  if (i > 0)
37828
- await sleep(INTER_QUERY_DELAY_MS);
38372
+ await sleep(GRAPHQL_INTER_QUERY_DELAY_MS);
37829
38373
  const query = buildQuery(buildLabelQuery(labelChunks[i]));
37830
- const data = await cachedSearchIssues(octokit, {
38374
+ const data = await searchIssuesGraphQLFirst(octokit, {
37831
38375
  q: query,
37832
38376
  sort: "created",
37833
38377
  order: "desc",
37834
38378
  per_page: perPage
37835
- });
38379
+ }, tracker);
37836
38380
  for (const item of data.items) {
37837
38381
  if (!seenUrls.has(item.html_url)) {
37838
38382
  seenUrls.add(item.html_url);
@@ -37851,14 +38395,16 @@ function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
37851
38395
  return [languages.map((l) => `language:${l}`).join(" ")];
37852
38396
  return languages.map((l) => `language:${l}`);
37853
38397
  }
37854
- async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage) {
37855
- 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)];
37856
38402
  const seenUrls = /* @__PURE__ */ new Set();
37857
38403
  const allItems = [];
37858
38404
  for (let i = 0; i < langVariants.length; i++) {
37859
38405
  if (i > 0)
37860
- await sleep(INTER_QUERY_DELAY_MS);
37861
- const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage);
38406
+ await sleep(GRAPHQL_INTER_QUERY_DELAY_MS);
38407
+ const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage, tracker);
37862
38408
  for (const item of items) {
37863
38409
  if (!seenUrls.has(item.html_url)) {
37864
38410
  seenUrls.add(item.html_url);
@@ -37872,7 +38418,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37872
38418
  const spamRepos = detectLabelFarmingRepos(items);
37873
38419
  if (spamRepos.size > 0) {
37874
38420
  const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
37875
- 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(", ")}`);
37876
38422
  }
37877
38423
  const itemsToVet = filterIssues(items).filter((item) => {
37878
38424
  const repoFullName = extractRepoFromUrl(item.repository_url);
@@ -37883,7 +38429,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37883
38429
  return excludedRepoSets.every((s) => !s.has(repoFullName));
37884
38430
  }).slice(0, remainingNeeded * 2);
37885
38431
  if (itemsToVet.length === 0) {
37886
- debug(MODULE10, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
38432
+ debug(MODULE11, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
37887
38433
  return { candidates: [], allVetFailed: false, rateLimitHit: false };
37888
38434
  }
37889
38435
  const { candidates: results, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
@@ -37895,7 +38441,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37895
38441
  });
37896
38442
  const starFilteredCount = results.length - starFiltered.length;
37897
38443
  if (starFilteredCount > 0) {
37898
- debug(MODULE10, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
38444
+ debug(MODULE11, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
37899
38445
  }
37900
38446
  return { candidates: starFiltered, allVetFailed, rateLimitHit };
37901
38447
  }
@@ -37903,18 +38449,27 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37903
38449
  // ../core/dist/core/personalization.js
37904
38450
  var REPO_BOOST = 20;
37905
38451
  var LANGUAGE_BOOST = 10;
38452
+ var ISSUE_TYPE_BOOST = 10;
38453
+ var AVOID_PENALTY = 15;
37906
38454
  function boostScoreOf(candidate) {
37907
38455
  return candidate.personalization?.kind === "boosted" ? candidate.personalization.score : 0;
37908
38456
  }
37909
- function annotateBoost(candidates, preferLanguages, preferRepos) {
37910
- const langSet = new Set((preferLanguages ?? []).map((l) => l.trim().toLowerCase()).filter(Boolean));
37911
- const repoSet = new Set((preferRepos ?? []).map((r) => r.trim().toLowerCase()).filter(Boolean));
37912
- if (langSet.size === 0 && repoSet.size === 0)
38457
+ function normalizeSet(values) {
38458
+ return new Set((values ?? []).map((v) => v.trim().toLowerCase()).filter(Boolean));
38459
+ }
38460
+ function annotateBoost(candidates, bias = {}) {
38461
+ const langSet = normalizeSet(bias.preferLanguages);
38462
+ const repoSet = normalizeSet(bias.preferRepos);
38463
+ const avoidSet = normalizeSet(bias.avoidRepos);
38464
+ const typeSet = normalizeSet(bias.boostIssueTypes);
38465
+ if (langSet.size === 0 && repoSet.size === 0 && avoidSet.size === 0 && typeSet.size === 0) {
37913
38466
  return candidates;
38467
+ }
37914
38468
  return candidates.map((c) => {
37915
38469
  let score = 0;
37916
38470
  const reasons = [];
37917
- if (repoSet.size > 0 && repoSet.has(c.issue.repo.toLowerCase())) {
38471
+ const repoLower = c.issue.repo.toLowerCase();
38472
+ if (repoSet.size > 0 && repoSet.has(repoLower)) {
37918
38473
  score += REPO_BOOST;
37919
38474
  reasons.push(`repo affinity: ${c.issue.repo}`);
37920
38475
  }
@@ -37923,7 +38478,18 @@ function annotateBoost(candidates, preferLanguages, preferRepos) {
37923
38478
  score += LANGUAGE_BOOST;
37924
38479
  reasons.push(`language match: ${lang}`);
37925
38480
  }
37926
- if (score === 0)
38481
+ if (typeSet.size > 0) {
38482
+ const matched = c.issue.labels.find((l) => typeSet.has(l.toLowerCase()));
38483
+ if (matched) {
38484
+ score += ISSUE_TYPE_BOOST;
38485
+ reasons.push(`issue type: ${matched}`);
38486
+ }
38487
+ }
38488
+ if (avoidSet.size > 0 && avoidSet.has(repoLower)) {
38489
+ score -= AVOID_PENALTY;
38490
+ reasons.push(`avoided repo: ${c.issue.repo}`);
38491
+ }
38492
+ if (reasons.length === 0)
37927
38493
  return c;
37928
38494
  return { ...c, personalization: { kind: "boosted", score, reasons } };
37929
38495
  });
@@ -37951,7 +38517,7 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
37951
38517
  break;
37952
38518
  if (seen.has(c.issue.url))
37953
38519
  continue;
37954
- if (boostScoreOf(c) > 0)
38520
+ if (boostScoreOf(c) !== 0)
37955
38521
  continue;
37956
38522
  picks.push({ ...c, personalization: { kind: "diversity" } });
37957
38523
  seen.add(c.issue.url);
@@ -37968,9 +38534,12 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
37968
38534
  }
37969
38535
 
37970
38536
  // ../core/dist/core/issue-discovery.js
37971
- var MODULE11 = "issue-discovery";
38537
+ var MODULE12 = "issue-discovery";
37972
38538
  var LOW_BUDGET_THRESHOLD = 20;
37973
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;
37974
38543
  function buildIssueFilter(config2) {
37975
38544
  return (items) => {
37976
38545
  return items.filter((item) => {
@@ -37992,6 +38561,8 @@ function buildIssueFilter(config2) {
37992
38561
  if (config2.skippedUrls.has(item.html_url))
37993
38562
  return false;
37994
38563
  const updatedAt = new Date(item.updated_at);
38564
+ if (Number.isNaN(updatedAt.getTime()))
38565
+ return false;
37995
38566
  const ageDays = daysBetween(updatedAt, config2.now);
37996
38567
  if (ageDays > config2.maxAgeDays)
37997
38568
  return false;
@@ -38002,9 +38573,9 @@ function buildIssueFilter(config2) {
38002
38573
  };
38003
38574
  }
38004
38575
  async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
38005
- info(MODULE11, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
38006
- const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
38007
- 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`);
38008
38579
  return {
38009
38580
  candidates,
38010
38581
  error: allReposFailed ? "All merged-PR repo fetches failed" : null,
@@ -38012,26 +38583,26 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
38012
38583
  };
38013
38584
  }
38014
38585
  async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
38015
- info(MODULE11, `Phase 1: Searching issues in ${repos.length} starred repos...`);
38586
+ info(MODULE12, `Phase 1: Searching issues in ${repos.length} starred repos...`);
38016
38587
  const phase1Labels = labels.slice(0, 3);
38017
38588
  const reposToSearch = repos.slice(0, 10);
38018
38589
  const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
38019
- info(MODULE11, `Found ${candidates.length} candidates from starred repos`);
38590
+ info(MODULE12, `Found ${candidates.length} candidates from starred repos`);
38020
38591
  return {
38021
38592
  candidates,
38022
38593
  error: allReposFailed ? "All starred repo fetches failed" : null,
38023
38594
  rateLimitHit
38024
38595
  };
38025
38596
  }
38026
- async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
38027
- 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...");
38028
38599
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38029
38600
  const tierLabelGroups = [];
38030
38601
  if (scopes && scopes.length > 1) {
38031
38602
  for (const scope of scopes) {
38032
38603
  const scopeLabels = SCOPE_LABELS[scope] ?? [];
38033
38604
  if (scopeLabels.length === 0) {
38034
- warn(MODULE11, `Scope "${scope}" has no labels, skipping tier`);
38605
+ warn(MODULE12, `Scope "${scope}" has no labels, skipping tier`);
38035
38606
  continue;
38036
38607
  }
38037
38608
  tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
@@ -38050,8 +38621,8 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38050
38621
  let rateLimitHit = false;
38051
38622
  for (const { tier, tierLabels } of tierLabelGroups) {
38052
38623
  try {
38053
- const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
38054
- 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...`);
38055
38626
  const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
38056
38627
  tierResults.push(tierCandidates);
38057
38628
  for (const c of tierCandidates)
@@ -38062,7 +38633,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38062
38633
  if (vetRateLimitHit) {
38063
38634
  rateLimitHit = true;
38064
38635
  }
38065
- info(MODULE11, `Found ${tierCandidates.length} candidates from ${tier} tier`);
38636
+ info(MODULE12, `Found ${tierCandidates.length} candidates from ${tier} tier`);
38066
38637
  } catch (err) {
38067
38638
  if (getHttpStatusCode(err) === 401)
38068
38639
  throw err;
@@ -38071,13 +38642,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38071
38642
  if (isRateLimitError(err)) {
38072
38643
  rateLimitHit = true;
38073
38644
  }
38074
- warn(MODULE11, `Error in ${tier} tier search: ${errMsg}`);
38645
+ warn(MODULE12, `Error in ${tier} tier search: ${errMsg}`);
38075
38646
  tierResults.push([]);
38076
38647
  }
38077
38648
  }
38078
38649
  const interleaved = interleaveArrays(tierResults);
38079
38650
  if (interleaved.length === 0 && error48) {
38080
- 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}`);
38081
38652
  }
38082
38653
  return {
38083
38654
  candidates: interleaved.slice(0, maxResults),
@@ -38085,18 +38656,18 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
38085
38656
  rateLimitHit
38086
38657
  };
38087
38658
  }
38088
- async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
38089
- info(MODULE11, "Phase 3: Searching actively maintained repos...");
38659
+ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues, tracker) {
38660
+ info(MODULE12, "Phase 3: Searching actively maintained repos...");
38090
38661
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
38091
38662
  const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
38092
38663
  if (eligibleStarred.length > 0) {
38093
- 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...`);
38094
38665
  const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
38095
38666
  if (restItems.length > 0) {
38096
38667
  try {
38097
38668
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
38098
38669
  if (candidates.length > 0) {
38099
- info(MODULE11, `Found ${candidates.length} candidates from maintained-repo REST search`);
38670
+ info(MODULE12, `Found ${candidates.length} candidates from maintained-repo REST search`);
38100
38671
  return {
38101
38672
  candidates,
38102
38673
  error: allVetFailed ? "all vetting failed" : null,
@@ -38106,11 +38677,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38106
38677
  } catch (error48) {
38107
38678
  if (getHttpStatusCode(error48) === 401)
38108
38679
  throw error48;
38109
- 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));
38110
38681
  }
38111
38682
  }
38112
38683
  }
38113
- info(MODULE11, "Phase 3: Falling back to Search API...");
38684
+ info(MODULE12, "Phase 3: Falling back to Search API...");
38114
38685
  const thirtyDaysAgo = /* @__PURE__ */ new Date();
38115
38686
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
38116
38687
  const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
@@ -38118,15 +38689,15 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38118
38689
  const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : "";
38119
38690
  const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
38120
38691
  try {
38121
- const data = await cachedSearchIssues(octokit, {
38692
+ const data = await searchIssuesGraphQLFirst(octokit, {
38122
38693
  q: phase3Query,
38123
38694
  sort: "updated",
38124
38695
  order: "desc",
38125
38696
  per_page: maxResults * 3
38126
- });
38127
- info(MODULE11, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
38697
+ }, tracker);
38698
+ info(MODULE12, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
38128
38699
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
38129
- info(MODULE11, `Found ${candidates.length} candidates from maintained-repo search`);
38700
+ info(MODULE12, `Found ${candidates.length} candidates from maintained-repo search`);
38130
38701
  return {
38131
38702
  candidates,
38132
38703
  error: allVetFailed ? "all vetting failed" : null,
@@ -38136,7 +38707,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
38136
38707
  if (getHttpStatusCode(error48) === 401)
38137
38708
  throw error48;
38138
38709
  const errMsg = errorMessage(error48);
38139
- warn(MODULE11, `Error in maintained-repo search: ${errMsg}`);
38710
+ warn(MODULE12, `Error in maintained-repo search: ${errMsg}`);
38140
38711
  return {
38141
38712
  candidates: [],
38142
38713
  error: errMsg,
@@ -38150,19 +38721,26 @@ var IssueDiscovery = class {
38150
38721
  octokit;
38151
38722
  githubToken;
38152
38723
  vetter;
38724
+ budgetTracker;
38153
38725
  /** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
38154
38726
  rateLimitWarning = null;
38155
38727
  /**
38156
38728
  * @param githubToken - GitHub personal access token or token from `gh auth token`
38157
38729
  * @param preferences - User's search preferences (languages, labels, scopes, etc.)
38158
38730
  * @param stateReader - Read-only interface for accessing scout state (merged PRs, starred repos, etc.)
38731
+ * @param budgetTracker - Search budget tracker. Defaults to the shared
38732
+ * singleton so existing callers behave identically. A long-lived host
38733
+ * serving concurrent searches can inject a per-search instance so one
38734
+ * search's init() no longer resets the budget state of another (the
38735
+ * shared-singleton concurrency hazard, #156).
38159
38736
  */
38160
- constructor(githubToken, preferences, stateReader) {
38737
+ constructor(githubToken, preferences, stateReader, budgetTracker = getSearchBudgetTracker()) {
38161
38738
  this.preferences = preferences;
38162
38739
  this.stateReader = stateReader;
38163
38740
  this.githubToken = githubToken;
38164
38741
  this.octokit = getOctokit(githubToken);
38165
- this.vetter = new IssueVetter(this.octokit, this.stateReader);
38742
+ this.budgetTracker = budgetTracker;
38743
+ this.vetter = new IssueVetter(this.octokit, this.stateReader, this.budgetTracker);
38166
38744
  }
38167
38745
  /**
38168
38746
  * Get starred repos from the state reader.
@@ -38211,9 +38789,23 @@ var IssueDiscovery = class {
38211
38789
  const allCandidates = [];
38212
38790
  const phaseErrors = {};
38213
38791
  let rateLimitHitDuringSearch = false;
38792
+ const applyInterPhaseDelay = async () => {
38793
+ if (interPhaseDelay > 0) {
38794
+ info(MODULE12, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38795
+ await sleep(interPhaseDelay);
38796
+ }
38797
+ };
38798
+ const recordPhaseResult = (key, result) => {
38799
+ allCandidates.push(...result.candidates);
38800
+ phaseErrors[key] = result.error;
38801
+ if (result.rateLimitHit)
38802
+ rateLimitHitDuringSearch = true;
38803
+ };
38804
+ const viableCandidateCount = () => allCandidates.filter((c) => c.recommendation !== "skip").length;
38214
38805
  this.rateLimitWarning = null;
38215
- const tracker = getSearchBudgetTracker();
38216
- let searchBudget = LOW_BUDGET_THRESHOLD - 1;
38806
+ resetGraphQLSearchQueryCount();
38807
+ const tracker = this.budgetTracker;
38808
+ let searchBudget = CRITICAL_BUDGET_THRESHOLD - 1;
38217
38809
  try {
38218
38810
  const rateLimit = await checkRateLimit(this.githubToken);
38219
38811
  searchBudget = rateLimit.remaining;
@@ -38221,22 +38813,22 @@ var IssueDiscovery = class {
38221
38813
  if (rateLimit.remaining < 5) {
38222
38814
  const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
38223
38815
  this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
38224
- warn(MODULE11, this.rateLimitWarning);
38816
+ warn(MODULE12, this.rateLimitWarning);
38225
38817
  }
38226
38818
  if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
38227
- 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)`);
38228
38820
  } else if (searchBudget < LOW_BUDGET_THRESHOLD) {
38229
- 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`);
38230
38822
  }
38231
38823
  } catch (error48) {
38232
38824
  if (getHttpStatusCode(error48) === 401)
38233
38825
  throw error48;
38234
38826
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
38235
- 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));
38236
38828
  }
38237
38829
  if (searchBudget <= 0) {
38238
- this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
38239
- 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);
38240
38832
  }
38241
38833
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
38242
38834
  const openPRRepos = this.stateReader.getReposWithOpenPRs();
@@ -38247,17 +38839,24 @@ var IssueDiscovery = class {
38247
38839
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
38248
38840
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist.map((r) => r.toLowerCase()));
38249
38841
  if (aiBlocklisted.size > 0) {
38250
- 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(", ")}`);
38251
38843
  }
38252
- const filterIssues = buildIssueFilter({
38844
+ const baseFilterConfig = {
38253
38845
  excludedRepos: new Set(config2.excludeRepos.map((r) => r.toLowerCase())),
38254
38846
  excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
38255
38847
  aiBlocklisted,
38256
38848
  lowScoringRepos,
38257
38849
  skippedUrls: options.skippedUrls ?? /* @__PURE__ */ new Set(),
38258
- maxAgeDays: config2.maxIssueAgeDays || 90,
38259
38850
  now: /* @__PURE__ */ new Date(),
38260
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
38261
38860
  });
38262
38861
  const seenPhase0 = /* @__PURE__ */ new Set();
38263
38862
  const phase0Repos = [];
@@ -38270,31 +38869,24 @@ var IssueDiscovery = class {
38270
38869
  break;
38271
38870
  }
38272
38871
  const phase0RepoSet = new Set(phase0Repos);
38872
+ const otherStrategiesCanRun = starredRepos.length > 0 && enabledStrategies.has("starred") || enabledStrategies.has("broad") || enabledStrategies.has("maintained");
38273
38873
  if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
38274
- 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);
38275
38876
  if (remaining > 0) {
38276
- const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
38277
- allCandidates.push(...result.candidates);
38278
- phaseErrors["0"] = result.error;
38279
- if (result.rateLimitHit)
38280
- rateLimitHitDuringSearch = true;
38877
+ const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssuesPhase0);
38878
+ recordPhaseResult("0", result);
38281
38879
  }
38282
38880
  strategiesUsed.push("merged");
38283
38881
  }
38284
38882
  if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
38285
- if (interPhaseDelay > 0) {
38286
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38287
- await sleep(interPhaseDelay);
38288
- }
38883
+ await applyInterPhaseDelay();
38289
38884
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
38290
38885
  if (reposToSearch.length > 0) {
38291
38886
  const remaining = maxResults - allCandidates.length;
38292
38887
  if (remaining > 0) {
38293
38888
  const result = await runPhase1(this.octokit, this.vetter, reposToSearch, labels, remaining, filterIssues);
38294
- allCandidates.push(...result.candidates);
38295
- phaseErrors["1"] = result.error;
38296
- if (result.rateLimitHit)
38297
- rateLimitHitDuringSearch = true;
38889
+ recordPhaseResult("1", result);
38298
38890
  strategiesUsed.push("starred");
38299
38891
  }
38300
38892
  }
@@ -38302,49 +38894,39 @@ var IssueDiscovery = class {
38302
38894
  const broadDelay = options.broadPhaseDelayMs ?? config2.broadPhaseDelayMs ?? 9e4;
38303
38895
  const configuredSkipThreshold = config2.skipBroadWhenSufficientResults ?? 8;
38304
38896
  const skipThreshold = configuredSkipThreshold > 0 ? Math.min(configuredSkipThreshold, maxResults - 1) : 0;
38305
- if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
38306
- if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
38307
- 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})`);
38308
38904
  } else {
38309
- if (interPhaseDelay > 0) {
38310
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38311
- await sleep(interPhaseDelay);
38312
- }
38905
+ await applyInterPhaseDelay();
38313
38906
  if (allCandidates.length > 0 && broadDelay > 0) {
38314
- 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...`);
38315
38908
  await sleep(broadDelay);
38316
38909
  } else if (allCandidates.length === 0) {
38317
- 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`);
38318
38911
  }
38319
- const remaining = maxResults - allCandidates.length;
38320
- const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
38321
- allCandidates.push(...result.candidates);
38322
- phaseErrors["2"] = result.error;
38323
- if (result.rateLimitHit)
38324
- rateLimitHitDuringSearch = true;
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);
38914
+ recordPhaseResult("2", result);
38325
38915
  strategiesUsed.push("broad");
38326
38916
  }
38327
38917
  }
38328
- if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
38329
- if (interPhaseDelay > 0) {
38330
- info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
38331
- await sleep(interPhaseDelay);
38332
- }
38333
- const remaining = maxResults - allCandidates.length;
38334
- const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues);
38335
- allCandidates.push(...result.candidates);
38336
- phaseErrors["3"] = result.error;
38337
- if (result.rateLimitHit)
38338
- rateLimitHitDuringSearch = true;
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")) {
38922
+ await applyInterPhaseDelay();
38923
+ const remaining = maxResults - viableBeforeMaintained;
38924
+ const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues, tracker);
38925
+ recordPhaseResult("3", result);
38339
38926
  strategiesUsed.push("maintained");
38340
38927
  }
38341
- const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
38342
- let budgetNote = "";
38343
- if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
38344
- budgetNote = ` Most search phases were skipped due to critically low API quota (${searchBudget} remaining).`;
38345
- } else if (phasesSkippedForBudget) {
38346
- budgetNote = ` Some search phases were skipped due to low API quota (${searchBudget} remaining).`;
38347
- }
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.` : "";
38348
38930
  if (allCandidates.length === 0) {
38349
38931
  const errorDetails = [
38350
38932
  phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
@@ -38357,12 +38939,17 @@ var IssueDiscovery = class {
38357
38939
  this.rateLimitWarning = `Search returned no results due to GitHub API rate limits.${details}${budgetNote} Try again after the rate limit resets.`;
38358
38940
  return { candidates: [], strategiesUsed };
38359
38941
  }
38360
- 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);
38361
38943
  }
38362
38944
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
38363
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.`;
38364
38946
  }
38365
- const ranked = annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
38947
+ const ranked = annotateBoost(allCandidates, {
38948
+ preferLanguages: options.preferLanguages,
38949
+ preferRepos: options.preferRepos,
38950
+ avoidRepos: options.avoidRepos,
38951
+ boostIssueTypes: options.boostIssueTypes
38952
+ });
38366
38953
  ranked.sort((a, b) => {
38367
38954
  const priorityOrder = {
38368
38955
  merged_pr: 0,
@@ -38381,9 +38968,13 @@ var IssueDiscovery = class {
38381
38968
  return boostDiff;
38382
38969
  return b.viabilityScore - a.viabilityScore;
38383
38970
  });
38384
- 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);
38385
38976
  const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
38386
- 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`);
38387
38978
  return { candidates: finalPicks, strategiesUsed };
38388
38979
  }
38389
38980
  /**
@@ -38416,7 +39007,6 @@ var IssueDiscovery = class {
38416
39007
  };
38417
39008
 
38418
39009
  // ../core/dist/core/roadmap.js
38419
- var MODULE12 = "roadmap";
38420
39010
  var CACHE_TTL_MS2 = 60 * 60 * 1e3;
38421
39011
  var ROADMAP_PATHS = [
38422
39012
  "ROADMAP.md",
@@ -38436,8 +39026,8 @@ function pruneCache2() {
38436
39026
  function parseRoadmapIssueRefs(content, owner, repo) {
38437
39027
  const refs = /* @__PURE__ */ new Set();
38438
39028
  const ownerRepoLower = `${owner}/${repo}`.toLowerCase();
38439
- const escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38440
- 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");
38441
39031
  for (const m of content.matchAll(fullRefPattern)) {
38442
39032
  const n = Number.parseInt(m[1], 10);
38443
39033
  if (n > 0)
@@ -38484,22 +39074,13 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
38484
39074
  var roadmapInflight = /* @__PURE__ */ new Map();
38485
39075
  async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
38486
39076
  for (const path5 of ROADMAP_PATHS) {
38487
- try {
38488
- const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
38489
- if (!("content" in data))
38490
- continue;
38491
- const content = Buffer.from(data.content, "base64").toString("utf-8");
38492
- const refs = parseRoadmapIssueRefs(content, owner, repo);
38493
- roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
38494
- pruneCache2();
38495
- return refs;
38496
- } catch (err) {
38497
- rethrowIfFatal(err);
38498
- const status = getHttpStatusCode(err);
38499
- if (status === 404)
38500
- continue;
38501
- warn(MODULE12, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(err)}`);
38502
- }
39077
+ const { text } = await probeRepoFile(octokit, owner, repo, path5);
39078
+ if (!text)
39079
+ continue;
39080
+ const refs = parseRoadmapIssueRefs(text, owner, repo);
39081
+ roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
39082
+ pruneCache2();
39083
+ return refs;
38503
39084
  }
38504
39085
  const empty = /* @__PURE__ */ new Set();
38505
39086
  roadmapCache.set(cacheKey, { refs: empty, fetchedAt: Date.now() });
@@ -38830,8 +39411,12 @@ var GistStateStore = class {
38830
39411
  let toWrite = state;
38831
39412
  try {
38832
39413
  const remote = await this.fetchGistState(this.gistId);
38833
- if (remote) {
38834
- 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;
38835
39420
  }
38836
39421
  } catch (err) {
38837
39422
  rethrowIfFatal(err);
@@ -38869,9 +39454,9 @@ var GistStateStore = class {
38869
39454
  debug(MODULE14, `Trying cached gist ID: ${cachedId}`);
38870
39455
  try {
38871
39456
  const fetched = await this.fetchGistState(cachedId);
38872
- if (fetched) {
39457
+ if (fetched.kind === "ok") {
38873
39458
  this.gistId = cachedId;
38874
- const state = this.mergeCacheInto(fetched);
39459
+ const state = this.mergeCacheInto(fetched.state);
38875
39460
  this.writeCache(state);
38876
39461
  return { gistId: cachedId, state, created: false };
38877
39462
  }
@@ -38888,8 +39473,8 @@ var GistStateStore = class {
38888
39473
  this.saveGistId(search.id);
38889
39474
  this.gistId = search.id;
38890
39475
  const fetched = await this.fetchGistState(search.id);
38891
- if (fetched) {
38892
- const state = this.mergeCacheInto(fetched);
39476
+ if (fetched.kind === "ok") {
39477
+ const state = this.mergeCacheInto(fetched.state);
38893
39478
  this.writeCache(state);
38894
39479
  return { gistId: search.id, state, created: false };
38895
39480
  }
@@ -38933,17 +39518,24 @@ var GistStateStore = class {
38933
39518
  };
38934
39519
  }
38935
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
+ */
38936
39528
  async fetchGistState(gistId) {
38937
39529
  const { data } = await this.octokit.gists.get({ gist_id: gistId });
38938
39530
  const file2 = data.files?.[GIST_FILENAME];
38939
39531
  if (!file2?.content)
38940
- return null;
39532
+ return { kind: "missing" };
38941
39533
  try {
38942
39534
  const parsed = JSON.parse(file2.content);
38943
- return parseScoutState(parsed);
39535
+ return { kind: "ok", state: parseScoutState(parsed) };
38944
39536
  } catch (err) {
38945
39537
  warn(MODULE14, `Gist content failed validation: ${errorMessage(err)}`);
38946
- return null;
39538
+ return { kind: "invalid" };
38947
39539
  }
38948
39540
  }
38949
39541
  /**
@@ -39037,7 +39629,7 @@ function mergeTombstones(local, remote) {
39037
39629
  }
39038
39630
  return [...byUrl.values()].filter((t) => {
39039
39631
  const ts = new Date(t.removedAt).getTime();
39040
- return !Number.isFinite(ts) || ts >= cutoff;
39632
+ return Number.isFinite(ts) && ts >= cutoff;
39041
39633
  });
39042
39634
  }
39043
39635
  function applyTombstones(items, tombstones, urlOf, touchedAtOf) {
@@ -39079,11 +39671,26 @@ function mergeStates(local, remote) {
39079
39671
  skippedIssues,
39080
39672
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
39081
39673
  lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
39082
- gistId: remote.gistId ?? local.gistId
39674
+ gistId: remote.gistId ?? local.gistId,
39675
+ searchRotation: mergeSearchRotation(local.searchRotation, remote.searchRotation)
39083
39676
  };
39084
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
+ }
39085
39692
  function mergeRepoScores(local, remote) {
39086
- const merged = { ...local };
39693
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), local);
39087
39694
  for (const [repo, remoteScore] of Object.entries(remote)) {
39088
39695
  const localScore = merged[repo];
39089
39696
  if (!localScore) {
@@ -39303,23 +39910,50 @@ var OssScout = class {
39303
39910
  this.evictStaleCacheEntries();
39304
39911
  this.cullExpiredSkips();
39305
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
+ }
39306
39922
  const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
39307
39923
  const prefs = this.state.preferences;
39308
39924
  const prefLangs = prefs.preferLanguages ?? [];
39309
39925
  const prefRepos = prefs.preferRepos ?? [];
39310
39926
  const preferLanguages = options?.preferLanguages ?? (prefLangs.length > 0 ? prefLangs : void 0);
39311
39927
  const preferRepos = options?.preferRepos ?? (prefRepos.length > 0 ? prefRepos : void 0);
39928
+ const prefAvoid = prefs.avoidRepos ?? [];
39929
+ const prefBoostTypes = prefs.boostIssueTypes ?? [];
39930
+ const avoidRepos = options?.avoidRepos ?? (prefAvoid.length > 0 ? prefAvoid : void 0);
39931
+ const boostIssueTypes = options?.boostIssueTypes ?? (prefBoostTypes.length > 0 ? prefBoostTypes : void 0);
39312
39932
  const diversityRatio = options?.diversityRatio ?? prefs.diversityRatio ?? 0;
39313
- const { candidates, strategiesUsed } = await discovery.searchIssues({
39314
- maxResults: options?.maxResults,
39315
- strategies: options?.strategies,
39316
- skippedUrls,
39317
- preferLanguages,
39318
- preferRepos,
39319
- diversityRatio,
39320
- interPhaseDelayMs: options?.interPhaseDelayMs,
39321
- broadPhaseDelayMs: options?.broadPhaseDelayMs
39322
- });
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);
39323
39957
  for (const c of candidates) {
39324
39958
  this.updateRepoSignalsFromHealth(c.projectHealth);
39325
39959
  }
@@ -39333,20 +39967,36 @@ var OssScout = class {
39333
39967
  strategiesUsed
39334
39968
  };
39335
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
+ }
39336
39987
  /**
39337
39988
  * Populate the `hasActiveMaintainers` repo-score signal from a freshly
39338
39989
  * computed projectHealth (#167). It was initialized false and never set, so
39339
39990
  * calculateScore's +1 active-maintainers weight was inert; `isActive`
39340
39991
  * (recent commit activity) is a real, already-computed proxy.
39341
39992
  *
39342
- * `isResponsive` and `avgResponseDays` are deliberately NOT set here:
39343
- * `projectHealth.avgIssueResponseDays` is a hardcoded `0` placeholder
39344
- * (repo-health.ts), so deriving responsiveness from it would award +1 to
39345
- * every repo — a fake signal worse than the inert one. Real responsiveness
39346
- * needs an actual response-time measurement (extra API calls), deferred.
39347
- * `hasHostileComments` likewise stays a host-settable capability (it needs
39348
- * comment sentiment, out of scope). A failed health check is skipped so its
39349
- * neutral-default fields don't pollute the score.
39993
+ * `isResponsive` and `avgResponseDays` are deliberately NOT set here, and
39994
+ * `isResponsive` no longer carries a score weight at all (#167): real
39995
+ * responsiveness needs an actual response-time measurement (extra API calls)
39996
+ * that is out of scope, and `hasActiveMaintainers` already covers the
39997
+ * activity proxy. `hasHostileComments` stays a host-settable capability (it
39998
+ * needs comment sentiment, out of scope). A failed health check is skipped so
39999
+ * its neutral-default fields don't pollute the score.
39350
40000
  */
39351
40001
  updateRepoSignalsFromHealth(health) {
39352
40002
  if (health.checkFailed)
@@ -39878,6 +40528,7 @@ var OssScout = class {
39878
40528
  }
39879
40529
  if (this.persistLocal) {
39880
40530
  try {
40531
+ this.state = mergeStates(this.state, loadLocalState());
39881
40532
  saveLocalState(this.state);
39882
40533
  } catch {
39883
40534
  return false;
@@ -39903,16 +40554,21 @@ var OssScout = class {
39903
40554
  });
39904
40555
  }
39905
40556
  /**
39906
- * Calculate repo score (1-10) from observed data.
40557
+ * Calculate repo score from observed data.
39907
40558
  * base 5, +1 per merged PR (max +3), -1 per closed-without-merge (max -3),
39908
- * +1 responsive, +1 active maintainers, -2 hostile comments, clamped 1-10
40559
+ * +1 active maintainers, -2 hostile comments, clamped to [1, 10].
40560
+ *
40561
+ * `isResponsive` is intentionally NOT scored (#167): nothing in oss-scout
40562
+ * ever computes it, so awarding +1 for it was dead weight that the
40563
+ * now-computed `hasActiveMaintainers` signal already covers as the activity
40564
+ * proxy. The field is retained on RepoSignals for backward compatibility but
40565
+ * no longer affects the score. With the current signals the reachable range
40566
+ * is [1, 9]; the upper clamp stays as defensive hygiene.
39909
40567
  */
39910
40568
  calculateScore(repoScore) {
39911
40569
  let score = 5;
39912
40570
  score += Math.min(repoScore.mergedPRCount, 3);
39913
40571
  score -= Math.min(repoScore.closedWithoutMergeCount, 3);
39914
- if (repoScore.signals.isResponsive)
39915
- score += 1;
39916
40572
  if (repoScore.signals.hasActiveMaintainers)
39917
40573
  score += 1;
39918
40574
  if (repoScore.signals.hasHostileComments)
@@ -39949,6 +40605,8 @@ var FIELD_CONFIGS = {
39949
40605
  preferLanguages: { type: "array" },
39950
40606
  preferRepos: { type: "array" },
39951
40607
  diversityRatio: { type: "float" },
40608
+ avoidRepos: { type: "array" },
40609
+ boostIssueTypes: { type: "array" },
39952
40610
  slmTriageModel: { type: "string" },
39953
40611
  slmTriageHost: { type: "string" },
39954
40612
  featuresAnchorThreshold: { type: "number" },
@@ -40007,6 +40665,8 @@ function applyPreferenceField(preferences, key, value) {
40007
40665
  const prefs = { ...preferences };
40008
40666
  switch (field.type) {
40009
40667
  case "string":
40668
+ if (key === "slmTriageHost")
40669
+ assertValidTriageHost(value);
40010
40670
  prefs[key] = value;
40011
40671
  break;
40012
40672
  case "boolean":
@@ -40067,6 +40727,10 @@ function validateUrl(url2) {
40067
40727
 
40068
40728
  // src/tools.ts
40069
40729
  var TOOL_TIMEOUT_MS = 3e5;
40730
+ var PERSIST_WARNING = "Checkpoint failed: changes were applied in memory but could not be persisted. They may be lost when the server restarts.";
40731
+ function withPersistWarning(payload, persisted) {
40732
+ return persisted ? payload : { ...payload, persistWarning: PERSIST_WARNING };
40733
+ }
40070
40734
  function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
40071
40735
  let timeoutId;
40072
40736
  const timer = new Promise((_, reject) => {
@@ -40082,7 +40746,7 @@ function registerTools(server, scout) {
40082
40746
  "search",
40083
40747
  "Search for open source issues matching your preferences",
40084
40748
  {
40085
- 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)"),
40086
40750
  strategies: external_exports3.string().optional().describe(
40087
40751
  "Comma-separated search strategies: merged, starred, broad, maintained, all"
40088
40752
  ),
@@ -40092,6 +40756,12 @@ function registerTools(server, scout) {
40092
40756
  preferRepos: external_exports3.array(external_exports3.string()).optional().describe(
40093
40757
  "Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."
40094
40758
  ),
40759
+ avoidRepos: external_exports3.array(external_exports3.string()).optional().describe(
40760
+ "Soft PENALTY (milder than excludeRepos) for candidates in one of these `owner/repo` slugs (#168). Pushes them down the ranking but does not remove them; a strong boost can still outweigh it."
40761
+ ),
40762
+ boostIssueTypes: external_exports3.array(external_exports3.string()).optional().describe(
40763
+ 'Soft-boost ranking for candidates whose issue labels match one of these types, case-insensitive (e.g. "bug", "good first issue") (#168). Not a filter; only reorders.'
40764
+ ),
40095
40765
  diversityRatio: external_exports3.number().min(0).max(1).optional().describe(
40096
40766
  "Diversity counterweight (#1244): fraction of result slots reserved for candidates that matched NEITHER preference list. Counters echo-chamber bias when boosts accumulate over time. 0 disables, 1 reserves every slot for diversity. Default 0."
40097
40767
  )
@@ -40101,16 +40771,29 @@ function registerTools(server, scout) {
40101
40771
  strategies,
40102
40772
  preferLanguages,
40103
40773
  preferRepos,
40774
+ avoidRepos,
40775
+ boostIssueTypes,
40104
40776
  diversityRatio
40105
40777
  }) => {
40106
40778
  try {
40107
- 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;
40108
40789
  const result = await withTimeout(
40109
40790
  scout.search({
40110
40791
  maxResults: maxResults ?? 10,
40111
40792
  strategies: parsedStrategies,
40112
40793
  preferLanguages,
40113
40794
  preferRepos,
40795
+ avoidRepos,
40796
+ boostIssueTypes,
40114
40797
  diversityRatio,
40115
40798
  // No fixed inter-phase sleeps in the request/response MCP context;
40116
40799
  // the budget tracker still paces the GitHub calls (#143)
@@ -40119,9 +40802,18 @@ function registerTools(server, scout) {
40119
40802
  })
40120
40803
  );
40121
40804
  scout.saveResults(result.candidates);
40122
- await scout.checkpoint();
40805
+ const persisted = await scout.checkpoint();
40123
40806
  return {
40124
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
40807
+ content: [
40808
+ {
40809
+ type: "text",
40810
+ text: JSON.stringify(
40811
+ withPersistWarning(result, persisted),
40812
+ null,
40813
+ 2
40814
+ )
40815
+ }
40816
+ ]
40125
40817
  };
40126
40818
  } catch (err) {
40127
40819
  const msg = err instanceof Error ? err.message : String(err);
@@ -40136,7 +40828,7 @@ function registerTools(server, scout) {
40136
40828
  "scout-features",
40137
40829
  "Surface feature-scoped contribution opportunities in repos where you have 3+ merged PRs",
40138
40830
  {
40139
- 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)"),
40140
40832
  anchorThreshold: external_exports3.number().int().min(1).max(50).optional().describe("Anchor threshold override (default 3)"),
40141
40833
  splitRatio: external_exports3.number().min(0).max(1).optional().describe("Quick-wins/bigger-bets split ratio (default 0.6)")
40142
40834
  },
@@ -40150,9 +40842,18 @@ function registerTools(server, scout) {
40150
40842
  })
40151
40843
  );
40152
40844
  scout.saveResults([...result.quickWins, ...result.biggerBets]);
40153
- await scout.checkpoint();
40845
+ const persisted = await scout.checkpoint();
40154
40846
  return {
40155
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
40847
+ content: [
40848
+ {
40849
+ type: "text",
40850
+ text: JSON.stringify(
40851
+ withPersistWarning(result, persisted),
40852
+ null,
40853
+ 2
40854
+ )
40855
+ }
40856
+ ]
40156
40857
  };
40157
40858
  } catch (err) {
40158
40859
  const msg = err instanceof Error ? err.message : String(err);
@@ -40324,10 +41025,19 @@ function registerTools(server, scout) {
40324
41025
  scout.updatePreferences({
40325
41026
  [key]: validated[key]
40326
41027
  });
40327
- await scout.checkpoint();
41028
+ const persisted = await scout.checkpoint();
40328
41029
  const updated = scout.getPreferences();
40329
41030
  return {
40330
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
41031
+ content: [
41032
+ {
41033
+ type: "text",
41034
+ text: JSON.stringify(
41035
+ withPersistWarning(updated, persisted),
41036
+ null,
41037
+ 2
41038
+ )
41039
+ }
41040
+ ]
40331
41041
  };
40332
41042
  }
40333
41043
  );
@@ -40385,7 +41095,7 @@ function registerResources(server, scout) {
40385
41095
  // src/index.ts
40386
41096
  var SERVER_INFO = {
40387
41097
  name: "oss-scout-mcp",
40388
- version: "0.9.0"
41098
+ version: "0.10.1"
40389
41099
  // x-release-please-version
40390
41100
  };
40391
41101
  function createServer(scout) {