@oss-scout/mcp 0.5.0 → 0.6.0

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