@oss-scout/mcp 0.13.1 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42269,6 +42269,12 @@ function isAuthError(error62) {
42269
42269
  return status === 403 && !isRateLimitError(error62);
42270
42270
  }
42271
42271
  function isRateLimitError(error62) {
42272
+ const e = error62;
42273
+ for (const gqlErrors of [e?.errors, e?.data?.errors]) {
42274
+ if (Array.isArray(gqlErrors) && gqlErrors.some((g) => g?.type === "RATE_LIMITED")) {
42275
+ return true;
42276
+ }
42277
+ }
42272
42278
  const status = getHttpStatusCode(error62);
42273
42279
  if (status === 429)
42274
42280
  return true;
@@ -42622,7 +42628,8 @@ var SearchBudgetTracker = class {
42622
42628
  this.reservedCalls++;
42623
42629
  return;
42624
42630
  }
42625
- const waitUntil = oldestInWindow + SEARCH_WINDOW_MS;
42631
+ const externalExhausted = this.knownRemaining - this.callsSinceReset - this.reservedCalls <= 0;
42632
+ const waitUntil = externalExhausted && this.resetAt > Date.now() ? Math.min(oldestInWindow + SEARCH_WINDOW_MS, this.resetAt) : oldestInWindow + SEARCH_WINDOW_MS;
42626
42633
  const waitMs = waitUntil - Date.now();
42627
42634
  if (waitMs > 0) {
42628
42635
  debug(MODULE3, `Budget full (${this.callTimestamps.length}/${EFFECTIVE_BUDGET} in window), waiting ${waitMs}ms`);
@@ -42826,6 +42833,7 @@ var SavedCandidateSchema = external_exports.looseObject({
42826
42833
  lastStatus: external_exports.enum(["still_available", "claimed", "closed", "has_pr"]).optional()
42827
42834
  });
42828
42835
  var PersistenceModeSchema = external_exports.enum(["local", "gist"]);
42836
+ var ComplexIssuesModeSchema = external_exports.enum(["off", "warm"]);
42829
42837
  var ScoutPreferencesSchema = external_exports.looseObject({
42830
42838
  githubUsername: external_exports.string().default(""),
42831
42839
  languages: external_exports.array(external_exports.string()).default(["any"]),
@@ -42888,6 +42896,17 @@ var ScoutPreferencesSchema = external_exports.looseObject({
42888
42896
  * local network.
42889
42897
  */
42890
42898
  slmTriageHost: external_exports.string().default(""),
42899
+ /**
42900
+ * Surface issues that are not starter-shaped. "warm" unlocks them in repos
42901
+ * where the user already has a merged PR, and nowhere else: there a PR
42902
+ * that merely mentions the issue no longer counts as an existing PR (only
42903
+ * one that closes it does), a maintainer-filed or triage-accepted issue
42904
+ * passes the requirements check without code in its body, and a
42905
+ * maintainer-filed issue is not dropped for a question-shaped or short
42906
+ * title. Unlocked candidates carry `complexityUnlocked: true`. "off" (the
42907
+ * default) keeps the same rules everywhere.
42908
+ */
42909
+ complexIssues: ComplexIssuesModeSchema.default("off"),
42891
42910
  /**
42892
42911
  * Minimum merged-PR count for a repo to qualify as an anchor in
42893
42912
  * `scout features` (#98). Lowering surfaces more anchors at the cost
@@ -42950,10 +42969,11 @@ var ScoutStateSchema = external_exports.looseObject({
42950
42969
  * consumer wraps at use time so a shrinking language list never needs
42951
42970
  * clamping here).
42952
42971
  *
42953
- * `phase0Offset`, `starredOffset` and `maintainedOffset` do the same for the
42954
- * repo lists those phases cap per run (#324, #333): each run searches the
42955
- * next window of the list instead of always the first N, so every
42956
- * contributed/starred repo gets searched within a few runs.
42972
+ * `starredOffset` and `maintainedOffset` do the same for the repo lists
42973
+ * those phases cap per run (#324, #333): each run searches the next window
42974
+ * of the list instead of always the first N, so every starred repo gets
42975
+ * searched within a few runs. `phase0Offset` is still advanced but no
42976
+ * longer read: the merged phase reads every contributed repo each run.
42957
42977
  *
42958
42978
  * `strategyOffset` is the round-robin cursor (#336): a position in
42959
42979
  * CONCRETE_STRATEGIES. A search runs the first usable strategy from there,
@@ -43019,15 +43039,23 @@ function isDocOnlyIssue(item) {
43019
43039
  return false;
43020
43040
  return nonEmptyLabels.every((n) => DOC_ONLY_LABELS.has(n));
43021
43041
  }
43042
+ var MAINTAINER_ASSOCIATIONS = /* @__PURE__ */ new Set([
43043
+ "OWNER",
43044
+ "MEMBER",
43045
+ "COLLABORATOR"
43046
+ ]);
43022
43047
  var NON_ACTIONABLE_LABELS = /* @__PURE__ */ new Set(["question", "discussion", "support"]);
43023
- function isNonActionableIssue(item) {
43048
+ var QUESTION_TITLE = /^(?:(?:how|why|what|when|where|which|who)\b|(?:is|are|does|do|can|could|should|would|will|has|have)\s+(?:i|we|you|it|this|that|these|those|the|a|an|any|anyone|someone|there|my|your|our)\b)/i;
43049
+ function isNonActionableIssue(item, opts = {}) {
43024
43050
  const labelNames = (item.labels ?? []).map((l) => (typeof l === "string" ? l : l.name || "").toLowerCase());
43025
43051
  if (labelNames.some((n) => NON_ACTIONABLE_LABELS.has(n)))
43026
43052
  return true;
43053
+ if (opts.labelsOnly)
43054
+ return false;
43027
43055
  const title = (item.title ?? "").trim();
43028
43056
  if (title.endsWith("?"))
43029
43057
  return true;
43030
- if (/^(how|why|what|when|where|which|who|is|are|does|do|can|could|should|would|will|has|have)\b/i.test(title))
43058
+ if (QUESTION_TITLE.test(title))
43031
43059
  return true;
43032
43060
  return title.split(/\s+/).filter(Boolean).length < 3;
43033
43061
  }
@@ -43570,6 +43598,18 @@ function isNotModifiedError(err) {
43570
43598
  return getHttpStatusCode(err) === 304;
43571
43599
  }
43572
43600
 
43601
+ // ../core/dist/core/linked-pr.js
43602
+ var STALLED_PR_THRESHOLD_DAYS = 30;
43603
+ function isLinkedPRStalled(linkedPR, now = /* @__PURE__ */ new Date(), thresholdDays = STALLED_PR_THRESHOLD_DAYS) {
43604
+ if (!linkedPR || linkedPR.state !== "open" || !linkedPR.updatedAt)
43605
+ return false;
43606
+ const updated = new Date(linkedPR.updatedAt);
43607
+ if (Number.isNaN(updated.getTime()))
43608
+ return false;
43609
+ const ageDays = (now.getTime() - updated.getTime()) / (1e3 * 60 * 60 * 24);
43610
+ return ageDays >= thresholdDays;
43611
+ }
43612
+
43573
43613
  // ../core/dist/core/issue-eligibility.js
43574
43614
  function isLinkedPREvent(e) {
43575
43615
  return e.event === "cross-referenced" && !!e.source?.issue?.pull_request;
@@ -43605,10 +43645,25 @@ var ISSUE_OBJECT = String.raw`(?:this\b|it\b|that\b|the\b|#\d+|issue\b)`;
43605
43645
  var FIRST_PERSON_CLAIM_PATTERNS = [
43606
43646
  new RegExp(String.raw`\bi\s*(?:'ll|will) take ${ISSUE_OBJECT}`),
43607
43647
  new RegExp(String.raw`\bi\s*(?:'d|would) (?:like|love) to work on ${ISSUE_OBJECT}`),
43648
+ // "I'd like to investigate and fix this", "I'd love to tackle it".
43649
+ new RegExp(String.raw`\bi\s*(?:'d|would) (?:like|love) to (?:\w+ and )?(?:fix|tackle|take on|pick up|implement) ${ISSUE_OBJECT}`),
43608
43650
  /\bi\s*(?:'m|am) on it\b/,
43609
43651
  /\bi\s*(?:'ll|will) submit a pr\b/,
43610
43652
  /\bassigned to me\b/
43611
43653
  ];
43654
+ var OFFER_SUBJECT = String.raw`(?:^\s*|\b(?:i\s*(?:'m|am|'d be|would be|'ll be|will be)|we\s*(?:'re|are|'d be|would be|'ll be|will be))\s+(?:(?:really|very|more than|absolutely|also)\s+)?)`;
43655
+ var OFFER_PATTERNS = [
43656
+ new RegExp(String.raw`${OFFER_SUBJECT}(?:happy|glad|willing|keen) to (?:open|send|submit|make|put up|raise|contribute|write) (?:a |the |an )?(?:[\w-]+ ){0,2}(?:pr|pull request|patch|fix)\b`),
43657
+ new RegExp(String.raw`${OFFER_SUBJECT}(?:happy|glad|willing|keen) to (?:have a go|take a stab)\b`),
43658
+ /\bi can (?:send|open|submit|put up) (?:this |it )?(?:as )?(?:a |the )?(?:pr|pull request|patch|fix)\b/,
43659
+ /\bi\s*(?:'ll|will) (?:open|send|put up|raise) a (?:pr|pull request)\b/,
43660
+ /\bi\s*(?:'d|would) love to contribute (?:this|it|a fix|the fix)\b/,
43661
+ /\bi plan to (?:make|send|submit|open) (?:a |the )?(?:[\w-]+ ){0,2}(?:pr|pull request|fix|change)\b/
43662
+ ];
43663
+ var OFFER_VETO = /\bnot\b|n't\b|\bbut\b|\bunless\b/;
43664
+ function stripNonStatements(body) {
43665
+ return body.replace(/<!--[\s\S]*?-->/g, " ").replace(/```[\s\S]*?```/g, " ").replace(/^\s*[-*] \[ \].*$/gm, " ").replace(/^\s*>.*$/gm, " ");
43666
+ }
43612
43667
  var GENERIC_WORKING_PATTERNS = [
43613
43668
  /\bworking on (?:this|it)\b/,
43614
43669
  /\bworking on a (?:fix|pr)\b/
@@ -43616,7 +43671,7 @@ var GENERIC_WORKING_PATTERNS = [
43616
43671
  var PERMISSION_CLAIM_PATTERN = new RegExp(String.raw`\b(?:can|may|could) i (?:work on|take) ${ISSUE_OBJECT}`);
43617
43672
  var NON_CLAIM_SUBJECTS = /\b(?:anyone|anybody|someone|somebody|who|whoever|nobody|no[- ]?one|not)\b/;
43618
43673
  function commentClaimsIssue(body) {
43619
- const clauses = body.toLowerCase().split(/(?<=[.!?])|\n+/);
43674
+ const clauses = stripNonStatements(body).toLowerCase().split(/(?<=[.!?])|\n+/);
43620
43675
  for (const clause of clauses) {
43621
43676
  if (PERMISSION_CLAIM_PATTERN.test(clause))
43622
43677
  return true;
@@ -43624,6 +43679,8 @@ function commentClaimsIssue(body) {
43624
43679
  continue;
43625
43680
  if (FIRST_PERSON_CLAIM_PATTERNS.some((p) => p.test(clause)))
43626
43681
  return true;
43682
+ if (!OFFER_VETO.test(clause) && OFFER_PATTERNS.some((p) => p.test(clause)))
43683
+ return true;
43627
43684
  if (NON_CLAIM_SUBJECTS.test(clause))
43628
43685
  continue;
43629
43686
  if (GENERIC_WORKING_PATTERNS.some((p) => p.test(clause)))
@@ -43631,8 +43688,67 @@ function commentClaimsIssue(body) {
43631
43688
  }
43632
43689
  return false;
43633
43690
  }
43634
- async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
43691
+ function toLinkedPR(pr) {
43692
+ return {
43693
+ number: pr.number,
43694
+ // A deleted account has no login; the PR still counts.
43695
+ author: pr.author?.login ?? "ghost",
43696
+ state: pr.state === "OPEN" ? "open" : "closed",
43697
+ merged: pr.state === "MERGED",
43698
+ url: pr.url,
43699
+ updatedAt: pr.updatedAt
43700
+ };
43701
+ }
43702
+ var CLOSING_PR_RANK = { MERGED: 0, OPEN: 1, CLOSED: 2 };
43703
+ async function checkNoClosingPR(octokit, owner, repo, issueNumber, username) {
43704
+ const data = await octokit.graphql(`query closingPRs($owner: String!, $repo: String!, $number: Int!) {
43705
+ repository(owner: $owner, name: $repo) {
43706
+ issue(number: $number) {
43707
+ closedByPullRequestsReferences(first: 20, includeClosedPrs: true) {
43708
+ nodes { number state url updatedAt author { login } }
43709
+ }
43710
+ timelineItems(itemTypes: [CROSS_REFERENCED_EVENT], last: 30) {
43711
+ nodes {
43712
+ ... on CrossReferencedEvent {
43713
+ source {
43714
+ ... on PullRequest { number state url updatedAt author { login } }
43715
+ }
43716
+ }
43717
+ }
43718
+ }
43719
+ }
43720
+ }
43721
+ }`, { owner, repo, number: issueNumber });
43722
+ const issue2 = data.repository?.issue;
43723
+ if (!issue2) {
43724
+ return {
43725
+ passed: true,
43726
+ inconclusive: true,
43727
+ reason: "issue not returned by GraphQL",
43728
+ linkedPR: null
43729
+ };
43730
+ }
43731
+ const nodes = (issue2.closedByPullRequestsReferences?.nodes ?? []).filter((n) => n != null);
43732
+ if (nodes.length > 0) {
43733
+ const pr = [...nodes].sort((a, b) => CLOSING_PR_RANK[a.state] - CLOSING_PR_RANK[b.state])[0];
43734
+ return { passed: false, linkedPR: toLinkedPR(pr) };
43735
+ }
43736
+ const openMentions = (issue2.timelineItems?.nodes ?? []).map((n) => n?.source).filter((s) => s?.state === "OPEN" && typeof s.number === "number" && typeof s.url === "string").map(toLinkedPR);
43737
+ const own2 = openMentions.find((pr) => username !== "" && pr.author.toLowerCase() === username.toLowerCase());
43738
+ if (own2)
43739
+ return { passed: false, linkedPR: own2 };
43740
+ const active = openMentions.filter((pr) => !isLinkedPRStalled(pr));
43741
+ return {
43742
+ passed: true,
43743
+ linkedPR: null,
43744
+ ...active.length > 0 ? { openMentions: active } : {}
43745
+ };
43746
+ }
43747
+ async function checkNoExistingPR(octokit, owner, repo, issueNumber, opts = {}) {
43635
43748
  try {
43749
+ if (opts.closingOnly) {
43750
+ return await checkNoClosingPR(octokit, owner, repo, issueNumber, opts.username ?? "");
43751
+ }
43636
43752
  const timeline = await paginateAll((page) => octokit.issues.listEventsForTimeline({
43637
43753
  owner,
43638
43754
  repo,
@@ -43747,9 +43863,15 @@ function analyzeRequirements(body) {
43747
43863
 
43748
43864
  // ../core/dist/core/probe-repo-file.js
43749
43865
  var MODULE6 = "probe-repo-file";
43866
+ var MISSING_FILE_TTL_MS = 4 * 60 * 60 * 1e3;
43750
43867
  async function probeRepoFile(octokit, owner, repo, path6) {
43868
+ const url2 = `/repos/${owner}/${repo}/contents/${path6}`;
43869
+ const missingKey = `missing:${url2}`;
43870
+ if (getHttpCache().getIfFresh(missingKey, MISSING_FILE_TTL_MS) !== null) {
43871
+ return { text: null, transient: false };
43872
+ }
43751
43873
  try {
43752
- const data = await cachedRequest(getHttpCache(), `/repos/${owner}/${repo}/contents/${path6}`, (headers) => octokit.repos.getContent({ owner, repo, path: path6, headers }));
43874
+ const data = await cachedRequest(getHttpCache(), url2, (headers) => octokit.repos.getContent({ owner, repo, path: path6, headers }));
43753
43875
  if (data && typeof data === "object" && "content" in data && typeof data.content === "string") {
43754
43876
  return {
43755
43877
  text: Buffer.from(data.content, "base64").toString("utf-8"),
@@ -43759,8 +43881,10 @@ async function probeRepoFile(octokit, owner, repo, path6) {
43759
43881
  return { text: null, transient: false };
43760
43882
  } catch (error62) {
43761
43883
  const status = getHttpStatusCode(error62);
43762
- if (status === 404)
43884
+ if (status === 404) {
43885
+ getHttpCache().set(missingKey, "", true);
43763
43886
  return { text: null, transient: false };
43887
+ }
43764
43888
  rethrowIfFatal(error62);
43765
43889
  warn(MODULE6, `Unexpected error fetching ${path6} from ${owner}/${repo}: ${errorMessage(error62)}`);
43766
43890
  return { text: null, transient: true };
@@ -43770,23 +43894,18 @@ async function probeRepoFile(octokit, owner, repo, path6) {
43770
43894
  // ../core/dist/core/repo-health.js
43771
43895
  var MODULE7 = "repo-health";
43772
43896
  var MERGE_WINDOW_DAYS = 90;
43773
- async function fetchRepoMergeStats(octokit, owner, repo, tracker) {
43897
+ async function fetchRepoMergeStats(octokit, owner, repo) {
43774
43898
  const since = new Date(Date.now() - MERGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
43775
- const count = async (qualifiers) => {
43776
- await tracker.waitForBudget();
43777
- try {
43778
- const { data } = await octokit.search.issuesAndPullRequests({
43779
- q: `repo:${owner}/${repo} ${qualifiers} closed:>=${since}`,
43780
- per_page: 1
43781
- // only total_count is needed
43782
- });
43783
- return data.total_count;
43784
- } finally {
43785
- tracker.recordCall();
43786
- }
43787
- };
43788
- const merged = await count("is:pr is:merged");
43789
- const closedUnmerged = await count("is:pr is:unmerged is:closed");
43899
+ const scope = `repo:${owner}/${repo} is:pr closed:>=${since}`;
43900
+ const data = await octokit.graphql(`query repoMergeStats($merged: String!, $closedUnmerged: String!) {
43901
+ merged: search(type: ISSUE, query: $merged, first: 0) { issueCount }
43902
+ closedUnmerged: search(type: ISSUE, query: $closedUnmerged, first: 0) { issueCount }
43903
+ }`, {
43904
+ merged: `${scope} is:merged`,
43905
+ closedUnmerged: `${scope} is:unmerged is:closed`
43906
+ });
43907
+ const merged = data.merged.issueCount;
43908
+ const closedUnmerged = data.closedUnmerged.issueCount;
43790
43909
  const denom = merged + closedUnmerged;
43791
43910
  return {
43792
43911
  recentMergedPRCount: merged,
@@ -43812,7 +43931,7 @@ function pruneCache() {
43812
43931
  }
43813
43932
  }
43814
43933
  }
43815
- async function checkProjectHealth(octokit, owner, repo, tracker = getSearchBudgetTracker()) {
43934
+ async function checkProjectHealth(octokit, owner, repo) {
43816
43935
  const cache = getHttpCache();
43817
43936
  const healthCacheKey = versionedCacheKey(`health:${owner}/${repo}`);
43818
43937
  try {
@@ -43830,7 +43949,7 @@ async function checkProjectHealth(octokit, owner, repo, tracker = getSearchBudge
43830
43949
  const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
43831
43950
  const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
43832
43951
  const ciStatus = "unknown";
43833
- const mergeStats = await fetchRepoMergeStats(octokit, owner, repo, tracker);
43952
+ const mergeStats = await fetchRepoMergeStats(octokit, owner, repo);
43834
43953
  return {
43835
43954
  repo: `${owner}/${repo}`,
43836
43955
  lastCommitAt,
@@ -43900,11 +44019,14 @@ async function fetchContributionGuidelinesUncached(octokit, owner, repo) {
43900
44019
  return guidelines;
43901
44020
  }
43902
44021
  }
43903
- guidelinesCache.set(cacheKey, {
43904
- guidelines: void 0,
43905
- fetchedAt: Date.now()
43906
- });
43907
- pruneCache();
44022
+ const transient = results.some((r) => r.status === "fulfilled" && r.value.transient);
44023
+ if (!transient) {
44024
+ guidelinesCache.set(cacheKey, {
44025
+ guidelines: void 0,
44026
+ fetchedAt: Date.now()
44027
+ });
44028
+ pruneCache();
44029
+ }
43908
44030
  return void 0;
43909
44031
  }
43910
44032
  function parseContributionGuidelines(content) {
@@ -43976,7 +44098,15 @@ var ANTI_LLM_KEYWORDS = [
43976
44098
  "no use of generative ai",
43977
44099
  "ban on ai-generated",
43978
44100
  "prohibit ai-generated",
43979
- "prohibits ai-generated"
44101
+ "prohibits ai-generated",
44102
+ "not be completed by automated agents",
44103
+ "must not be done by automated agents",
44104
+ "tasks are for humans only",
44105
+ // AGENTS.md bans (e.g. servo). Most AGENTS.md files are agent guides
44106
+ // that welcome AI work, so only outright refusals are listed.
44107
+ "do not generate code",
44108
+ "explicitly rejected the generative use of agents",
44109
+ "you are explicitly forbidden from modifying"
43980
44110
  ];
43981
44111
  function scanForAntiLLMPolicy(text) {
43982
44112
  if (!text)
@@ -44004,6 +44134,16 @@ var SOURCE_FILE_FAMILIES = [
44004
44134
  "code_of_conduct.md"
44005
44135
  ]
44006
44136
  },
44137
+ {
44138
+ // Agent instruction files; matched only on the refusal keywords above.
44139
+ canonical: "AGENTS.md",
44140
+ paths: ["AGENTS.md"]
44141
+ },
44142
+ {
44143
+ // Repos that state an AI policy in a dedicated file (e.g. cot-rs/cot).
44144
+ canonical: "AI_POLICY.md",
44145
+ paths: ["AI_POLICY.md", ".github/AI_POLICY.md"]
44146
+ },
44007
44147
  {
44008
44148
  canonical: "README.md",
44009
44149
  paths: ["README.md", "readme.md", "Readme.md"]
@@ -44082,6 +44222,11 @@ async function fetchAndScanAntiLLMPolicy(octokit, owner, repo, options) {
44082
44222
 
44083
44223
  // ../core/dist/core/issue-graphql.js
44084
44224
  var MODULE8 = "issue-graphql";
44225
+ function looksLikeBotLogin(login) {
44226
+ if (!login)
44227
+ return false;
44228
+ return /(?:^|-)bot(?:-|$)/i.test(login);
44229
+ }
44085
44230
  function issueCoreKey(owner, repo, number4) {
44086
44231
  return `${owner}/${repo}#${number4}`;
44087
44232
  }
@@ -44094,7 +44239,11 @@ function normalizeNode(node2) {
44094
44239
  labels: node2.labels.nodes.map((l) => l.name),
44095
44240
  commentCount: node2.comments.totalCount,
44096
44241
  createdAt: node2.createdAt,
44097
- updatedAt: node2.updatedAt
44242
+ updatedAt: node2.updatedAt,
44243
+ authorLogin: node2.author?.login,
44244
+ authorIsBot: node2.author?.__typename === "Bot" || /\[bot\]$/i.test(node2.author?.login ?? ""),
44245
+ authorLooksLikeBot: looksLikeBotLogin(node2.author?.login),
44246
+ authorIsMaintainer: MAINTAINER_ASSOCIATIONS.has(node2.authorAssociation ?? "")
44098
44247
  };
44099
44248
  }
44100
44249
  async function prefetchIssueCores(octokit, issues) {
@@ -44122,6 +44271,8 @@ async function prefetchIssueCores(octokit, issues) {
44122
44271
  comments { totalCount }
44123
44272
  createdAt
44124
44273
  updatedAt
44274
+ author { login __typename }
44275
+ authorAssociation
44125
44276
  }
44126
44277
  }`);
44127
44278
  });
@@ -44231,7 +44382,7 @@ async function graphqlSearchIssues(octokit, query, first) {
44231
44382
  title
44232
44383
  updatedAt
44233
44384
  labels(first: 20) { nodes { name } }
44234
- repository { nameWithOwner }
44385
+ repository { nameWithOwner stargazerCount }
44235
44386
  }
44236
44387
  }
44237
44388
  }
@@ -44268,7 +44419,8 @@ async function graphqlSearchIssues(octokit, query, first) {
44268
44419
  repository_url: `https://api.github.com/repos/${nameWithOwner}`,
44269
44420
  updated_at: node2.updatedAt ?? "",
44270
44421
  title: node2.title,
44271
- labels: node2.labels?.nodes ?? []
44422
+ labels: node2.labels?.nodes ?? [],
44423
+ repositoryStars: node2.repository?.stargazerCount
44272
44424
  });
44273
44425
  }
44274
44426
  return { total_count: search.issueCount ?? 0, items };
@@ -44432,6 +44584,9 @@ function isValidTriageShape(value) {
44432
44584
  var MODULE10 = "issue-vetting";
44433
44585
  var MAX_CONCURRENT_VETTING = 3;
44434
44586
  var VETTING_CACHE_TTL_MS = 15 * 60 * 1e3;
44587
+ var DECIDED_LABEL = /\b(?:accepted|confirmed|approved|triaged|planned|help[- _]wanted)\b/i;
44588
+ var DECIDED_LABEL_VETO = /\b(?:not|needs?|to[- ]be|wontfix|duplicate|cla|answer)\b/i;
44589
+ var UNDECIDED_MARKER = /\b(?:tracking|epic|meta|rfc|roadmap|blocked|on[- ]hold|question|discussion|support|needs?[- ](?:info|design|discussion|triage|decision))\b/i;
44435
44590
  function repoAcceptsContributionsFromHealth(projectHealth) {
44436
44591
  if (projectHealth.checkFailed || projectHealth.recentMergedPRCount == null) {
44437
44592
  return null;
@@ -44507,6 +44662,8 @@ function deriveRecommendation(input2) {
44507
44662
  if (input2.issueClosed) {
44508
44663
  reasonsToSkip.push("Issue is closed");
44509
44664
  }
44665
+ if (input2.notTakeableReason)
44666
+ reasonsToSkip.push(input2.notTakeableReason);
44510
44667
  let recommendation;
44511
44668
  if (input2.issueClosed) {
44512
44669
  recommendation = "skip";
@@ -44514,6 +44671,8 @@ function deriveRecommendation(input2) {
44514
44671
  recommendation = "skip";
44515
44672
  } else if (input2.ownPR) {
44516
44673
  recommendation = "skip";
44674
+ } else if (input2.notTakeableReason) {
44675
+ recommendation = "skip";
44517
44676
  } else if (input2.passedAllChecks && input2.repoAcceptsContributions !== false) {
44518
44677
  recommendation = "approve";
44519
44678
  } else if (reasonsToSkip.length > 2) {
@@ -44526,6 +44685,11 @@ function deriveRecommendation(input2) {
44526
44685
  recommendation = "needs_review";
44527
44686
  notes.push("Recommendation downgraded: one or more checks were inconclusive");
44528
44687
  }
44688
+ if (input2.cautionReasons?.length) {
44689
+ reasonsToSkip.push(...input2.cautionReasons);
44690
+ if (recommendation === "approve")
44691
+ recommendation = "needs_review";
44692
+ }
44529
44693
  return { notes, reasonsToApprove, reasonsToSkip, recommendation };
44530
44694
  }
44531
44695
  var IssueVetter = class {
@@ -44556,7 +44720,9 @@ var IssueVetter = class {
44556
44720
  async vetIssue(issueUrl, opts) {
44557
44721
  const cache = getHttpCache();
44558
44722
  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}` : "";
44559
- const cacheKey = versionedCacheKey(`vet:${issueUrl}${sigKey}`);
44723
+ const complexMode = this.stateReader.getComplexIssuesMode?.() ?? "off";
44724
+ const modeKey = complexMode === "off" ? "" : `:cx-${complexMode}`;
44725
+ const cacheKey = versionedCacheKey(`vet:${issueUrl}${sigKey}${modeKey}`);
44560
44726
  const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
44561
44727
  if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
44562
44728
  debug(MODULE10, `Vetting cache hit for ${issueUrl}`);
@@ -44570,23 +44736,45 @@ var IssueVetter = class {
44570
44736
  const repoFullName = `${owner}/${repo}`;
44571
44737
  const core = opts?.prefetched ?? await this.fetchIssueCore(owner, repo, number4);
44572
44738
  const reposWithMergedPRs = this.stateReader.getReposWithMergedPRs();
44573
- const hasMergedPRsInRepo = reposWithMergedPRs.includes(repoFullName);
44739
+ const repoLower = repoFullName.toLowerCase();
44740
+ const hasMergedPRsInRepo = reposWithMergedPRs.some((r) => r.toLowerCase() === repoLower);
44741
+ const complexityUnlocked = complexMode === "warm" && hasMergedPRsInRepo;
44742
+ const username = this.stateReader.getGitHubUsername?.() ?? "";
44574
44743
  const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
44575
- checkNoExistingPR(this.octokit, owner, repo, number4),
44744
+ checkNoExistingPR(this.octokit, owner, repo, number4, {
44745
+ closingOnly: complexityUnlocked,
44746
+ username
44747
+ }),
44576
44748
  checkNotClaimed(this.octokit, owner, repo, number4, core.commentCount),
44577
- checkProjectHealth(this.octokit, owner, repo, this.budgetTracker),
44749
+ checkProjectHealth(this.octokit, owner, repo),
44578
44750
  fetchContributionGuidelines(this.octokit, owner, repo),
44579
44751
  hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo, this.budgetTracker)
44580
44752
  ]);
44581
44753
  const antiLLMPolicy = await fetchAndScanAntiLLMPolicy(this.octokit, owner, repo, { contributingText: contributionGuidelines?.rawContent });
44582
44754
  const noExistingPR = existingPRCheck.passed;
44583
44755
  const notClaimed = claimCheck.passed;
44584
- const username = this.stateReader.getGitHubUsername?.() ?? "";
44756
+ const isOwnIssue = username !== "" && core.authorLogin?.toLowerCase() === username.toLowerCase();
44757
+ let notTakeableReason;
44758
+ if (core.authorIsBot) {
44759
+ notTakeableReason = "Filed by a bot account";
44760
+ } else if (!isOwnIssue && commentClaimsIssue(core.body)) {
44761
+ notTakeableReason = "Reporter offered to send the fix";
44762
+ }
44763
+ const cautionReasons = [];
44764
+ if (!core.authorIsBot && core.authorLooksLikeBot)
44765
+ cautionReasons.push("Author login looks like a bot");
44766
+ const openMentions = existingPRCheck.openMentions ?? [];
44767
+ if (openMentions.length > 0)
44768
+ cautionReasons.push(`Open PR ${openMentions.map((pr) => `#${pr.number}`).join(", ")} mentions this issue`);
44769
+ if (antiLLMPolicy.matched)
44770
+ cautionReasons.push(`AI contribution policy in ${antiLLMPolicy.sourceFile ?? "repo docs"}`);
44585
44771
  const linkedPR = existingPRCheck.linkedPR;
44586
44772
  const ownPR = !noExistingPR && !!linkedPR && linkedPR.state === "open" && !linkedPR.merged && username !== "" && linkedPR.author.toLowerCase() === username.toLowerCase();
44587
44773
  const linkedPRMerged = !!linkedPR && linkedPR.merged;
44588
44774
  const linkedPRClosed = !!linkedPR && linkedPR.state === "closed" && !linkedPR.merged;
44589
- const clearRequirements = analyzeRequirements(core.body);
44775
+ const bodyIsClear = analyzeRequirements(core.body);
44776
+ const maintainerDecided = complexityUnlocked && !bodyIsClear && core.body.trim().length >= 50 && ![core.title, ...core.labels].some((t) => UNDECIDED_MARKER.test(t)) && (!!core.authorIsMaintainer && !isOwnIssue || core.labels.some((l) => DECIDED_LABEL.test(l) && !DECIDED_LABEL_VETO.test(l)));
44777
+ const clearRequirements = bodyIsClear || maintainerDecided;
44590
44778
  const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
44591
44779
  const repoAcceptsContributions = repoAcceptsContributionsFromHealth(projectHealth);
44592
44780
  const vettingResult = {
@@ -44647,9 +44835,13 @@ var IssueVetter = class {
44647
44835
  matchesCategory,
44648
44836
  issueClosed,
44649
44837
  passedAllChecks: vettingResult.passedAllChecks,
44650
- repoAcceptsContributions
44838
+ repoAcceptsContributions,
44839
+ notTakeableReason,
44840
+ cautionReasons
44651
44841
  });
44652
44842
  vettingResult.notes = notes;
44843
+ if (maintainerDecided)
44844
+ notes.push("Requirements taken as decided: maintainer-filed or triage-accepted");
44653
44845
  const repoQualityBonus = projectHealth.checkFailed ? 0 : calculateRepoQualityBonus(projectHealth.stargazersCount ?? 0, projectHealth.forksCount ?? 0);
44654
44846
  if (projectHealth.checkFailed && repoQualityBonus === 0) {
44655
44847
  vettingResult.notes.push("Repo quality bonus unavailable: could not fetch star/fork counts due to API error");
@@ -44698,7 +44890,8 @@ var IssueVetter = class {
44698
44890
  reasonsToSkip,
44699
44891
  reasonsToApprove,
44700
44892
  viabilityScore,
44701
- searchPriority
44893
+ searchPriority,
44894
+ ...complexityUnlocked ? { complexityUnlocked: true } : {}
44702
44895
  };
44703
44896
  if (!hasInconclusiveChecks) {
44704
44897
  cache.set(cacheKey, "", result);
@@ -44724,7 +44917,11 @@ var IssueVetter = class {
44724
44917
  labels: ghIssue.labels.map((l) => typeof l === "string" ? l : l.name || ""),
44725
44918
  commentCount: ghIssue.comments,
44726
44919
  createdAt: ghIssue.created_at,
44727
- updatedAt: ghIssue.updated_at
44920
+ updatedAt: ghIssue.updated_at,
44921
+ authorLogin: ghIssue.user?.login,
44922
+ authorIsBot: ghIssue.user?.type === "Bot" || /\[bot\]$/i.test(ghIssue.user?.login ?? ""),
44923
+ authorLooksLikeBot: looksLikeBotLogin(ghIssue.user?.login),
44924
+ authorIsMaintainer: MAINTAINER_ASSOCIATIONS.has(ghIssue.author_association ?? "")
44728
44925
  };
44729
44926
  }
44730
44927
  /**
@@ -44913,7 +45110,7 @@ async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTrac
44913
45110
  return data;
44914
45111
  }
44915
45112
  async function searchIssuesGraphQLFirst(octokit, params, tracker = getSearchBudgetTracker()) {
44916
- const cacheKey = versionedCacheKey(`gqlsearch:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
45113
+ const cacheKey = versionedCacheKey(`gqlsearch2:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
44917
45114
  const cache = getHttpCache();
44918
45115
  const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
44919
45116
  if (cached2) {
@@ -45096,14 +45293,22 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
45096
45293
  const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
45097
45294
  debug(MODULE11, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
45098
45295
  }
45296
+ let preVetStarDropped = 0;
45099
45297
  const itemsToVet = filterIssues(items).filter((item) => {
45100
45298
  const repoFullName = extractRepoFromUrl(item.repository_url);
45101
45299
  if (!repoFullName)
45102
45300
  return false;
45103
45301
  if (spamRepos.has(repoFullName))
45104
45302
  return false;
45303
+ if (typeof item.repositoryStars === "number" && item.repositoryStars < minStars) {
45304
+ preVetStarDropped++;
45305
+ return false;
45306
+ }
45105
45307
  return excludedRepoSets.every((s) => !s.has(repoFullName));
45106
45308
  }).slice(0, remainingNeeded * 2);
45309
+ if (preVetStarDropped > 0) {
45310
+ debug(MODULE11, `[STAR_FILTER] Dropped ${preVetStarDropped} ${phaseLabel} items below ${minStars} stars before vetting`);
45311
+ }
45107
45312
  if (itemsToVet.length === 0) {
45108
45313
  debug(MODULE11, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
45109
45314
  return { candidates: [], allVetFailed: false, rateLimitHit: false };
@@ -45122,6 +45327,216 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
45122
45327
  return { candidates: starFiltered, allVetFailed, rateLimitHit };
45123
45328
  }
45124
45329
 
45330
+ // ../core/dist/core/warm-repos.js
45331
+ var MODULE12 = "warm-repos";
45332
+ var BATCH_SIZE = 8;
45333
+ var ISSUES_PER_REPO = 30;
45334
+ var INTER_BATCH_DELAY_MS = 500;
45335
+ var MAX_DEAD_BATCHES = 2;
45336
+ var GH_NAME_PATTERN2 = /^[A-Za-z0-9_.-]+$/;
45337
+ var VET_PER_REPO = KNOWN_REPO_PER_REPO_CAP + 1;
45338
+ var MAX_VETS = 60;
45339
+ var SKIP_MEMORY_MS = 24 * 60 * 60 * 1e3;
45340
+ var PERMANENT_ERROR_TYPES = /* @__PURE__ */ new Set(["NOT_FOUND", "FORBIDDEN"]);
45341
+ async function queryRepos(octokit, repos, noRetry = false) {
45342
+ const varDefs = [];
45343
+ const selections = [];
45344
+ const variables = {};
45345
+ repos.forEach((fullName, i) => {
45346
+ const [owner, name] = fullName.split("/");
45347
+ varDefs.push(`$o${i}: String!, $n${i}: String!`);
45348
+ variables[`o${i}`] = owner;
45349
+ variables[`n${i}`] = name;
45350
+ selections.push(`r${i}: repository(owner: $o${i}, name: $n${i}) {
45351
+ nameWithOwner
45352
+ isArchived
45353
+ stargazerCount
45354
+ issues(first: ${ISSUES_PER_REPO}, states: OPEN, orderBy: { field: CREATED_AT, direction: DESC }) {
45355
+ nodes {
45356
+ url
45357
+ title
45358
+ createdAt
45359
+ updatedAt
45360
+ authorAssociation
45361
+ viewerDidAuthor
45362
+ assignees { totalCount }
45363
+ comments { totalCount }
45364
+ labels(first: 10) { nodes { name } }
45365
+ closedByPullRequestsReferences(first: 1, includeClosedPrs: false) { totalCount }
45366
+ }
45367
+ }
45368
+ }`);
45369
+ });
45370
+ const query = `query warmRepoIssues(${varDefs.join(", ")}) {
45371
+ ${selections.join("\n")}
45372
+ }`;
45373
+ try {
45374
+ return {
45375
+ data: await octokit.graphql(query, {
45376
+ ...variables,
45377
+ ...noRetry ? { request: { retries: 0 } } : {}
45378
+ }),
45379
+ permanent: /* @__PURE__ */ new Set()
45380
+ };
45381
+ } catch (err) {
45382
+ const partial2 = err.data;
45383
+ if (!partial2 || isRateLimitError(err))
45384
+ throw err;
45385
+ const gqlErrors = err.errors ?? [];
45386
+ return {
45387
+ data: partial2,
45388
+ permanent: new Set(gqlErrors.filter((e) => PERMANENT_ERROR_TYPES.has(e.type ?? "")).map((e) => String(e.path?.[0])))
45389
+ };
45390
+ }
45391
+ }
45392
+ function takeableIssues(listedAs, node2, minStars) {
45393
+ const nodes = node2?.issues?.nodes;
45394
+ if (!node2 || !nodes)
45395
+ return null;
45396
+ if (node2.isArchived)
45397
+ return [];
45398
+ if ((node2.stargazerCount ?? 0) < minStars)
45399
+ return [];
45400
+ const repo = node2.nameWithOwner;
45401
+ return nodes.flatMap((n) => {
45402
+ if (!n)
45403
+ return [];
45404
+ if (n.assignees.totalCount > 0)
45405
+ return [];
45406
+ if ((n.closedByPullRequestsReferences?.totalCount ?? 0) > 0)
45407
+ return [];
45408
+ return [
45409
+ {
45410
+ html_url: n.url,
45411
+ repository_url: `https://api.github.com/repos/${repo}`,
45412
+ updated_at: n.updatedAt,
45413
+ title: n.title,
45414
+ labels: (n.labels?.nodes ?? []).map((l) => ({ name: l.name })),
45415
+ repo,
45416
+ listedAs,
45417
+ createdAt: n.createdAt,
45418
+ maintainerFiled: MAINTAINER_ASSOCIATIONS.has(n.authorAssociation) && !n.viewerDidAuthor,
45419
+ commentCount: n.comments.totalCount
45420
+ }
45421
+ ];
45422
+ });
45423
+ }
45424
+ async function listWarmRepoIssues(octokit, repos, minStars = 0) {
45425
+ const valid = repos.filter((r) => {
45426
+ const parts = r.split("/");
45427
+ return parts.length === 2 && parts.every((p) => GH_NAME_PATTERN2.test(p));
45428
+ });
45429
+ const byUrl = /* @__PURE__ */ new Map();
45430
+ let failedRepos = repos.length - valid.length;
45431
+ let rateLimitHit = false;
45432
+ const read = async (batch, noRetry = false) => {
45433
+ try {
45434
+ const { data, permanent } = await queryRepos(octokit, batch, noRetry);
45435
+ const retry2 = [];
45436
+ batch.forEach((repo, j) => {
45437
+ const found = takeableIssues(repo, data[`r${j}`], minStars);
45438
+ if (found)
45439
+ for (const issue2 of found)
45440
+ byUrl.set(issue2.html_url, issue2);
45441
+ else if (batch.length > 1 && !permanent.has(`r${j}`))
45442
+ retry2.push(repo);
45443
+ else
45444
+ failedRepos++;
45445
+ });
45446
+ return retry2;
45447
+ } catch (err) {
45448
+ if (getHttpStatusCode(err) === 401)
45449
+ throw err;
45450
+ if (isRateLimitError(err)) {
45451
+ warn(MODULE12, `Rate limit hit listing warm repos: ${errorMessage(err)}`);
45452
+ rateLimitHit = true;
45453
+ return [];
45454
+ }
45455
+ warn(MODULE12, `Failed to list issues for ${batch.join(", ")}: ${errorMessage(err)}`);
45456
+ return null;
45457
+ }
45458
+ };
45459
+ let deadBatches = 0;
45460
+ for (let i = 0; i < valid.length && !rateLimitHit; i += BATCH_SIZE) {
45461
+ if (i > 0)
45462
+ await sleep(INTER_BATCH_DELAY_MS);
45463
+ const batch = valid.slice(i, i + BATCH_SIZE);
45464
+ const first = await read(batch);
45465
+ let alive = first !== null;
45466
+ if (first === null && batch.length === 1)
45467
+ failedRepos++;
45468
+ for (const repo of first ?? (batch.length > 1 ? batch : [])) {
45469
+ if (rateLimitHit)
45470
+ break;
45471
+ if (await read([repo], first === null) === null)
45472
+ failedRepos++;
45473
+ else
45474
+ alive = true;
45475
+ }
45476
+ deadBatches = alive ? 0 : deadBatches + 1;
45477
+ if (deadBatches >= MAX_DEAD_BATCHES) {
45478
+ warn(MODULE12, `${deadBatches} batches in a row returned nothing; stopping the warm-repo walk`);
45479
+ failedRepos += Math.max(0, valid.length - (i + batch.length));
45480
+ break;
45481
+ }
45482
+ }
45483
+ return { items: [...byUrl.values()], failedRepos, rateLimitHit };
45484
+ }
45485
+ var DAY_MS = 24 * 60 * 60 * 1e3;
45486
+ function rankWarmIssues(items, repoScore, now = /* @__PURE__ */ new Date()) {
45487
+ const score = (issue2) => {
45488
+ const ageDays = (now.getTime() - Date.parse(issue2.createdAt)) / DAY_MS;
45489
+ const freshness = ageDays <= 2 ? 3 : ageDays <= 14 ? 2 : ageDays <= 30 ? 1 : 0;
45490
+ return freshness + (issue2.maintainerFiled ? 3 : 0) + (issue2.commentCount === 0 ? 2 : 0) + (repoScore(issue2.repo) ?? repoScore(issue2.listedAs) ?? 5) / 2;
45491
+ };
45492
+ return items.map((issue2) => ({ issue: issue2, score: score(issue2) })).sort((a, b) => b.score - a.score || Date.parse(b.issue.createdAt) - Date.parse(a.issue.createdAt)).map((entry) => entry.issue);
45493
+ }
45494
+ var skipMemoryKey = (url2) => versionedCacheKey(`warm-skip:${url2}`);
45495
+ async function fetchWarmRepoCandidates(octokit, vetter, repos, maxResults, filterFn, repoScore, minStars = 0) {
45496
+ const cache = getHttpCache();
45497
+ const listed = await listWarmRepoIssues(octokit, repos, minStars);
45498
+ const passing = (items) => new Set(filterFn(items).map((i) => i.html_url));
45499
+ const kept = passing(listed.items);
45500
+ const keptAsListed = passing(listed.items.map((i) => ({
45501
+ ...i,
45502
+ repository_url: `https://api.github.com/repos/${i.listedAs}`
45503
+ })));
45504
+ const ranked = rankWarmIssues(listed.items.filter((i) => kept.has(i.html_url) && keptAsListed.has(i.html_url) && cache.getEntryIfFresh(skipMemoryKey(i.html_url), SKIP_MEMORY_MS) === null), repoScore);
45505
+ const toVet = applyPerRepoCap(ranked.map((i) => ({ url: i.html_url, issue: { repo: i.repo } })), VET_PER_REPO).slice(0, Math.min(maxResults * 2, MAX_VETS)).map((i) => i.url);
45506
+ let candidates = [];
45507
+ let rateLimitHit = listed.rateLimitHit;
45508
+ let vetError = null;
45509
+ if (toVet.length > 0 && !listed.rateLimitHit) {
45510
+ try {
45511
+ const vetted = await vetter.vetIssuesParallel(toVet, toVet.length, "merged_pr");
45512
+ rateLimitHit ||= vetted.rateLimitHit;
45513
+ for (const c of vetted.candidates) {
45514
+ if (c.recommendation === "skip") {
45515
+ cache.set(skipMemoryKey(c.issue.url), "", true);
45516
+ }
45517
+ }
45518
+ const rank = new Map(toVet.map((url2, i) => [url2, i]));
45519
+ const byRank = [...vetted.candidates].sort((a, b) => (rank.get(a.issue.url) ?? 0) - (rank.get(b.issue.url) ?? 0));
45520
+ candidates = applyPerRepoCap([
45521
+ ...byRank.filter((c) => c.recommendation !== "skip"),
45522
+ ...byRank.filter((c) => c.recommendation === "skip")
45523
+ ], KNOWN_REPO_PER_REPO_CAP).slice(0, maxResults);
45524
+ } catch (err) {
45525
+ if (getHttpStatusCode(err) === 401)
45526
+ throw err;
45527
+ rateLimitHit ||= isRateLimitError(err);
45528
+ vetError = errorMessage(err);
45529
+ warn(MODULE12, `Vetting warm-repo issues failed: ${vetError}`);
45530
+ }
45531
+ }
45532
+ return {
45533
+ candidates,
45534
+ allReposFailed: repos.length > 0 && listed.failedRepos >= repos.length,
45535
+ rateLimitHit,
45536
+ vetError
45537
+ };
45538
+ }
45539
+
45125
45540
  // ../core/dist/core/personalization.js
45126
45541
  var REPO_BOOST = 20;
45127
45542
  var LANGUAGE_BOOST = 10;
@@ -45210,13 +45625,12 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
45210
45625
  }
45211
45626
 
45212
45627
  // ../core/dist/core/issue-discovery.js
45213
- var MODULE12 = "issue-discovery";
45628
+ var MODULE13 = "issue-discovery";
45214
45629
  var LOW_BUDGET_THRESHOLD = 20;
45215
45630
  var CRITICAL_BUDGET_THRESHOLD = 10;
45216
- var PHASE0_PER_PAGE = 30;
45217
45631
  var CONTRIBUTED_REPO_MAX_AGE_DAYS = 365;
45218
- var PHASE0_MAX_SHARE = 0.5;
45219
- var PHASE0_REPOS_PER_RUN = 10;
45632
+ var PHASE0_MAX_SHARE = 0.8;
45633
+ var PHASE0_MAX_REPOS = 200;
45220
45634
  var STARRED_REPOS_PER_RUN = 10;
45221
45635
  var MAINTAINED_REPOS_PER_RUN = 15;
45222
45636
  function rotatingWindow(items, size, offset) {
@@ -45253,34 +45667,35 @@ function buildIssueFilter(config2) {
45253
45667
  return false;
45254
45668
  if (!config2.includeDocIssues && isDocOnlyIssue(item))
45255
45669
  return false;
45256
- if (isNonActionableIssue(item))
45670
+ const labelsOnly = item.maintainerFiled === true && config2.complexRepos.has(repoLower);
45671
+ if (isNonActionableIssue(item, { labelsOnly }))
45257
45672
  return false;
45258
45673
  return true;
45259
45674
  });
45260
45675
  };
45261
45676
  }
45262
- async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
45263
- info(MODULE12, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter, ${PHASE0_PER_PAGE}/repo)...`);
45264
- const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues, PHASE0_PER_PAGE);
45265
- info(MODULE12, `Found ${candidates.length} candidates from merged-PR repos`);
45677
+ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues, repoScore, minStars) {
45678
+ info(MODULE13, `Phase 0: Listing open issues in ${repos.length} merged-PR repos (no label filter)...`);
45679
+ const { candidates, allReposFailed, rateLimitHit, vetError } = await fetchWarmRepoCandidates(octokit, vetter, repos, maxResults, filterIssues, repoScore, minStars);
45680
+ info(MODULE13, `Found ${candidates.length} candidates from merged-PR repos`);
45266
45681
  return {
45267
45682
  candidates,
45268
- error: allReposFailed ? "All merged-PR repo fetches failed" : null,
45683
+ error: allReposFailed ? "All merged-PR repo fetches failed" : vetError,
45269
45684
  rateLimitHit
45270
45685
  };
45271
45686
  }
45272
45687
  var MAX_QUERY_ORGS = 8;
45273
45688
  async function runPhaseOrgs(octokit, vetter, orgs, languages, isAnyLanguage, labels, maxResults, minStars, phase0RepoSet, starredRepoSet, filterIssues, tracker) {
45274
- info(MODULE12, `Orgs phase: searching ${orgs.length} preferred org(s)...`);
45689
+ info(MODULE13, `Orgs phase: searching ${orgs.length} preferred org(s)...`);
45275
45690
  const queryOrgs = orgs.slice(0, MAX_QUERY_ORGS);
45276
45691
  if (orgs.length > MAX_QUERY_ORGS) {
45277
- warn(MODULE12, `Orgs phase: capping query at ${MAX_QUERY_ORGS} org(s); dropping ${orgs.slice(MAX_QUERY_ORGS).join(", ")}`);
45692
+ warn(MODULE13, `Orgs phase: capping query at ${MAX_QUERY_ORGS} org(s); dropping ${orgs.slice(MAX_QUERY_ORGS).join(", ")}`);
45278
45693
  }
45279
45694
  const orgQuery = queryOrgs.map((o) => `org:${o}`).join(" ");
45280
45695
  try {
45281
45696
  const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, (langQ) => `is:issue is:open ${langQ} no:assignee ${orgQuery}`.replace(/ +/g, " ").trim(), maxResults * 3, tracker, 0, queryOrgs.length);
45282
45697
  const { candidates, allVetFailed, rateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet], maxResults, minStars, "Orgs phase");
45283
- info(MODULE12, `Found ${candidates.length} candidates from preferred orgs`);
45698
+ info(MODULE13, `Found ${candidates.length} candidates from preferred orgs`);
45284
45699
  return {
45285
45700
  candidates,
45286
45701
  error: allVetFailed ? "all vetting failed" : null,
@@ -45290,7 +45705,7 @@ async function runPhaseOrgs(octokit, vetter, orgs, languages, isAnyLanguage, lab
45290
45705
  if (getHttpStatusCode(error62) === 401)
45291
45706
  throw error62;
45292
45707
  const errMsg = errorMessage(error62);
45293
- warn(MODULE12, `Error in preferred-orgs search: ${errMsg}`);
45708
+ warn(MODULE13, `Error in preferred-orgs search: ${errMsg}`);
45294
45709
  return {
45295
45710
  candidates: [],
45296
45711
  error: errMsg,
@@ -45299,11 +45714,11 @@ async function runPhaseOrgs(octokit, vetter, orgs, languages, isAnyLanguage, lab
45299
45714
  }
45300
45715
  }
45301
45716
  async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
45302
- info(MODULE12, `Phase 1: Searching issues in ${repos.length} starred repos...`);
45717
+ info(MODULE13, `Phase 1: Searching issues in ${repos.length} starred repos...`);
45303
45718
  const phase1Labels = labels.slice(0, 3);
45304
45719
  const reposToSearch = repos.slice(0, STARRED_REPOS_PER_RUN);
45305
45720
  const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
45306
- info(MODULE12, `Found ${candidates.length} candidates from starred repos`);
45721
+ info(MODULE13, `Found ${candidates.length} candidates from starred repos`);
45307
45722
  return {
45308
45723
  candidates,
45309
45724
  error: allReposFailed ? "All starred repo fetches failed" : null,
@@ -45311,14 +45726,14 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
45311
45726
  };
45312
45727
  }
45313
45728
  async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues, tracker, languageRotationOffset = 0) {
45314
- info(MODULE12, "Phase 2: General issue search...");
45729
+ info(MODULE13, "Phase 2: General issue search...");
45315
45730
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
45316
45731
  const tierLabelGroups = [];
45317
45732
  if (scopes && scopes.length > 1) {
45318
45733
  for (const scope of scopes) {
45319
45734
  const scopeLabels = SCOPE_LABELS[scope] ?? [];
45320
45735
  if (scopeLabels.length === 0) {
45321
- warn(MODULE12, `Scope "${scope}" has no labels, skipping tier`);
45736
+ warn(MODULE13, `Scope "${scope}" has no labels, skipping tier`);
45322
45737
  continue;
45323
45738
  }
45324
45739
  tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
@@ -45338,7 +45753,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
45338
45753
  for (const { tier, tierLabels } of tierLabelGroups) {
45339
45754
  try {
45340
45755
  const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3, tracker, languageRotationOffset);
45341
- info(MODULE12, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
45756
+ info(MODULE13, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
45342
45757
  const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
45343
45758
  tierResults.push(tierCandidates);
45344
45759
  for (const c of tierCandidates)
@@ -45349,7 +45764,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
45349
45764
  if (vetRateLimitHit) {
45350
45765
  rateLimitHit = true;
45351
45766
  }
45352
- info(MODULE12, `Found ${tierCandidates.length} candidates from ${tier} tier`);
45767
+ info(MODULE13, `Found ${tierCandidates.length} candidates from ${tier} tier`);
45353
45768
  } catch (err) {
45354
45769
  if (getHttpStatusCode(err) === 401)
45355
45770
  throw err;
@@ -45358,13 +45773,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
45358
45773
  if (isRateLimitError(err)) {
45359
45774
  rateLimitHit = true;
45360
45775
  }
45361
- warn(MODULE12, `Error in ${tier} tier search: ${errMsg}`);
45776
+ warn(MODULE13, `Error in ${tier} tier search: ${errMsg}`);
45362
45777
  tierResults.push([]);
45363
45778
  }
45364
45779
  }
45365
45780
  const interleaved = interleaveArrays(tierResults);
45366
45781
  if (interleaved.length === 0 && error62) {
45367
- warn(MODULE12, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error62}`);
45782
+ warn(MODULE13, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error62}`);
45368
45783
  }
45369
45784
  return {
45370
45785
  candidates: interleaved.slice(0, maxResults),
@@ -45373,17 +45788,17 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
45373
45788
  };
45374
45789
  }
45375
45790
  async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues, tracker, rotationOffset = 0) {
45376
- info(MODULE12, "Phase 3: Searching actively maintained repos...");
45791
+ info(MODULE13, "Phase 3: Searching actively maintained repos...");
45377
45792
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
45378
45793
  const eligibleStarred = rotatingWindow(starredRepos, MAINTAINED_REPOS_PER_RUN, rotationOffset).filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
45379
45794
  if (eligibleStarred.length > 0) {
45380
- info(MODULE12, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
45795
+ info(MODULE13, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
45381
45796
  const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred, minStars, maxResults);
45382
45797
  if (restItems.length > 0) {
45383
45798
  try {
45384
45799
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
45385
45800
  if (candidates.length > 0) {
45386
- info(MODULE12, `Found ${candidates.length} candidates from maintained-repo REST search`);
45801
+ info(MODULE13, `Found ${candidates.length} candidates from maintained-repo REST search`);
45387
45802
  return {
45388
45803
  candidates,
45389
45804
  error: allVetFailed ? "all vetting failed" : null,
@@ -45393,11 +45808,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
45393
45808
  } catch (error62) {
45394
45809
  if (getHttpStatusCode(error62) === 401)
45395
45810
  throw error62;
45396
- warn(MODULE12, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error62));
45811
+ warn(MODULE13, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error62));
45397
45812
  }
45398
45813
  }
45399
45814
  }
45400
- info(MODULE12, "Phase 3: Falling back to Search API...");
45815
+ info(MODULE13, "Phase 3: Falling back to Search API...");
45401
45816
  const thirtyDaysAgo = /* @__PURE__ */ new Date();
45402
45817
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
45403
45818
  const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
@@ -45411,9 +45826,9 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
45411
45826
  order: "desc",
45412
45827
  per_page: maxResults * 3
45413
45828
  }, tracker);
45414
- info(MODULE12, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
45829
+ info(MODULE13, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
45415
45830
  const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
45416
- info(MODULE12, `Found ${candidates.length} candidates from maintained-repo search`);
45831
+ info(MODULE13, `Found ${candidates.length} candidates from maintained-repo search`);
45417
45832
  return {
45418
45833
  candidates,
45419
45834
  error: allVetFailed ? "all vetting failed" : null,
@@ -45423,7 +45838,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
45423
45838
  if (getHttpStatusCode(error62) === 401)
45424
45839
  throw error62;
45425
45840
  const errMsg = errorMessage(error62);
45426
- warn(MODULE12, `Error in maintained-repo search: ${errMsg}`);
45841
+ warn(MODULE13, `Error in maintained-repo search: ${errMsg}`);
45427
45842
  return {
45428
45843
  candidates: [],
45429
45844
  error: errMsg,
@@ -45507,7 +45922,7 @@ var IssueDiscovery = class {
45507
45922
  let rateLimitHitDuringSearch = false;
45508
45923
  const applyInterPhaseDelay = async () => {
45509
45924
  if (interPhaseDelay > 0) {
45510
- info(MODULE12, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
45925
+ info(MODULE13, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
45511
45926
  await sleep(interPhaseDelay);
45512
45927
  }
45513
45928
  };
@@ -45530,23 +45945,23 @@ var IssueDiscovery = class {
45530
45945
  if (rateLimit.remaining < 5) {
45531
45946
  const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
45532
45947
  this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
45533
- warn(MODULE12, this.rateLimitWarning);
45948
+ warn(MODULE13, this.rateLimitWarning);
45534
45949
  }
45535
45950
  if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
45536
- info(MODULE12, `Search budget critical (${searchBudget} remaining) \u2014 skipping the starred phase; broad/maintained still run on GraphQL (points bucket)`);
45951
+ info(MODULE13, `Search budget critical (${searchBudget} remaining) \u2014 skipping the starred phase; broad/maintained still run on GraphQL (points bucket)`);
45537
45952
  } else if (searchBudget < LOW_BUDGET_THRESHOLD) {
45538
- info(MODULE12, `Search budget low (${searchBudget} remaining) \u2014 broad/maintained run on GraphQL (points bucket), unaffected by REST search quota`);
45953
+ info(MODULE13, `Search budget low (${searchBudget} remaining) \u2014 broad/maintained run on GraphQL (points bucket), unaffected by REST search quota`);
45539
45954
  }
45540
45955
  } catch (error62) {
45541
45956
  if (getHttpStatusCode(error62) === 401)
45542
45957
  throw error62;
45543
45958
  preflightFailed = true;
45544
45959
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
45545
- warn(MODULE12, "Could not check rate limit \u2014 using conservative budget, skipping the starred phase:", errorMessage(error62));
45960
+ warn(MODULE13, "Could not check rate limit \u2014 using conservative budget, skipping the starred phase:", errorMessage(error62));
45546
45961
  }
45547
45962
  if (searchBudget <= 0) {
45548
45963
  this.rateLimitWarning = "GitHub search API quota exhausted \u2014 skipping the starred phase; other phases don't use it.";
45549
- warn(MODULE12, this.rateLimitWarning);
45964
+ warn(MODULE13, this.rateLimitWarning);
45550
45965
  }
45551
45966
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
45552
45967
  const openPRRepos = this.stateReader.getReposWithOpenPRs();
@@ -45557,7 +45972,7 @@ var IssueDiscovery = class {
45557
45972
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
45558
45973
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist.map((r) => r.toLowerCase()));
45559
45974
  if (aiBlocklisted.size > 0) {
45560
- debug(MODULE12, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
45975
+ debug(MODULE13, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
45561
45976
  }
45562
45977
  const baseFilterConfig = {
45563
45978
  excludedRepos: new Set(config2.excludeRepos.map((r) => r.toLowerCase())),
@@ -45566,7 +45981,8 @@ var IssueDiscovery = class {
45566
45981
  lowScoringRepos,
45567
45982
  skippedUrls: options.skippedUrls ?? /* @__PURE__ */ new Set(),
45568
45983
  now: /* @__PURE__ */ new Date(),
45569
- includeDocIssues: config2.includeDocIssues ?? true
45984
+ includeDocIssues: config2.includeDocIssues ?? true,
45985
+ complexRepos: new Set(config2.complexIssues === "warm" ? mergedPRRepos.map((r) => r.toLowerCase()) : [])
45570
45986
  };
45571
45987
  const filterIssues = buildIssueFilter({
45572
45988
  ...baseFilterConfig,
@@ -45577,10 +45993,10 @@ var IssueDiscovery = class {
45577
45993
  maxAgeDays: CONTRIBUTED_REPO_MAX_AGE_DAYS
45578
45994
  });
45579
45995
  const repoRotation = options.repoRotationOffsets ?? {};
45580
- const phase0Repos = rotatingWindow([.../* @__PURE__ */ new Set([...mergedPRRepos, ...openPRRepos])], PHASE0_REPOS_PER_RUN, repoRotation.merged ?? 0);
45996
+ const phase0Repos = enabledStrategies.has("merged") ? [.../* @__PURE__ */ new Set([...mergedPRRepos, ...openPRRepos])].slice(0, PHASE0_MAX_REPOS) : [];
45581
45997
  const phase0RepoSet = new Set(phase0Repos);
45582
45998
  const preferredOrgs = config2.preferredOrgs ?? [];
45583
- const starredToSearch = rotatingWindow(starredRepos, STARRED_REPOS_PER_RUN, repoRotation.starred ?? 0).filter((r) => !phase0RepoSet.has(r));
45999
+ const starredToSearch = rotatingWindow(starredRepos.filter((r) => !phase0RepoSet.has(r)), STARRED_REPOS_PER_RUN, repoRotation.starred ?? 0);
45584
46000
  const canRun = {
45585
46001
  all: false,
45586
46002
  merged: phase0Repos.length > 0,
@@ -45592,7 +46008,7 @@ var IssueDiscovery = class {
45592
46008
  const broadDelay = options.broadPhaseDelayMs ?? config2.broadPhaseDelayMs ?? 0;
45593
46009
  const runStrategy = {
45594
46010
  merged: async (wanted) => {
45595
- recordPhaseResult("0", await runPhase0(this.octokit, this.vetter, phase0Repos, wanted, filterIssuesPhase0));
46011
+ recordPhaseResult("0", await runPhase0(this.octokit, this.vetter, phase0Repos, wanted, filterIssuesPhase0, (repo) => this.stateReader.getRepoScore(repo), minStars));
45596
46012
  strategiesUsed.push("merged");
45597
46013
  },
45598
46014
  orgs: async (wanted) => {
@@ -45627,16 +46043,16 @@ var IssueDiscovery = class {
45627
46043
  }
45628
46044
  if (strategiesUsed.length > 0)
45629
46045
  await applyInterPhaseDelay();
45630
- info(MODULE12, `Round-robin: running the ${strategy} strategy`);
46046
+ info(MODULE13, `Round-robin: running the ${strategy} strategy`);
45631
46047
  await runStrategy[strategy](maxResults - viableCandidateCount());
45632
46048
  if (viableCandidateCount() > 0)
45633
46049
  break;
45634
46050
  }
45635
46051
  } else {
45636
46052
  const orgsPhaseEnabled = canRun.orgs && enabledStrategies.has("orgs");
45637
- const otherStrategiesCanRun = orgsPhaseEnabled || starredRepos.length > 0 && enabledStrategies.has("starred") || enabledStrategies.has("broad") || enabledStrategies.has("maintained");
46053
+ const otherStrategiesCanRun = orgsPhaseEnabled || canRun.starred && enabledStrategies.has("starred") || enabledStrategies.has("broad") || enabledStrategies.has("maintained");
45638
46054
  if (canRun.merged && enabledStrategies.has("merged")) {
45639
- const phase0Cap = otherStrategiesCanRun ? Math.max(1, Math.ceil(maxResults * PHASE0_MAX_SHARE)) : maxResults;
46055
+ const phase0Cap = otherStrategiesCanRun ? Math.max(1, Math.min(Math.ceil(maxResults * PHASE0_MAX_SHARE), maxResults - 1)) : maxResults;
45640
46056
  await runStrategy.merged(Math.min(maxResults - allCandidates.length, phase0Cap));
45641
46057
  }
45642
46058
  if (allCandidates.length < maxResults && orgsPhaseEnabled) {
@@ -45653,26 +46069,26 @@ var IssueDiscovery = class {
45653
46069
  const skipThreshold = configuredSkipThreshold > 0 ? Math.min(configuredSkipThreshold, maxResults - 1) : 0;
45654
46070
  const viableBeforeBroad = viableCandidateCount();
45655
46071
  if (viableBeforeBroad >= maxResults && enabledStrategies.has("broad")) {
45656
- info(MODULE12, `Skipping broad search: ${viableBeforeBroad} viable candidate(s) already meet maxResults (${maxResults})`);
46072
+ info(MODULE13, `Skipping broad search: ${viableBeforeBroad} viable candidate(s) already meet maxResults (${maxResults})`);
45657
46073
  } else if (enabledStrategies.has("broad")) {
45658
46074
  const preferredOrgSet = new Set(preferredOrgs.map((o) => o.toLowerCase()));
45659
46075
  const newRepoCandidateCount = allCandidates.filter((c) => c.recommendation !== "skip" && !phase0RepoSet.has(c.issue.repo) && !starredRepoSet.has(c.issue.repo) && !preferredOrgSet.has(c.issue.repo.split("/")[0]?.toLowerCase() ?? "")).length;
45660
46076
  if (skipThreshold > 0 && newRepoCandidateCount >= skipThreshold) {
45661
- info(MODULE12, `Skipping broad search: already found ${newRepoCandidateCount} viable candidate(s) from new repos (threshold: ${skipThreshold})`);
46077
+ info(MODULE13, `Skipping broad search: already found ${newRepoCandidateCount} viable candidate(s) from new repos (threshold: ${skipThreshold})`);
45662
46078
  } else {
45663
46079
  await applyInterPhaseDelay();
45664
46080
  if (allCandidates.length > 0 && broadDelay > 0) {
45665
- info(MODULE12, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
46081
+ info(MODULE13, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
45666
46082
  await sleep(broadDelay);
45667
46083
  } else if (allCandidates.length === 0) {
45668
- info(MODULE12, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
46084
+ info(MODULE13, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
45669
46085
  }
45670
46086
  await runStrategy.broad(maxResults - viableBeforeBroad);
45671
46087
  }
45672
46088
  }
45673
46089
  const viableBeforeMaintained = viableCandidateCount();
45674
46090
  if (viableBeforeMaintained >= maxResults && enabledStrategies.has("maintained")) {
45675
- info(MODULE12, `Skipping maintained search: ${viableBeforeMaintained} viable candidate(s) already meet maxResults (${maxResults})`);
46091
+ info(MODULE13, `Skipping maintained search: ${viableBeforeMaintained} viable candidate(s) already meet maxResults (${maxResults})`);
45676
46092
  } else if (enabledStrategies.has("maintained")) {
45677
46093
  await applyInterPhaseDelay();
45678
46094
  await runStrategy.maintained(maxResults - viableBeforeMaintained);
@@ -45728,7 +46144,7 @@ var IssueDiscovery = class {
45728
46144
  ];
45729
46145
  const capped = applyPerRepoCap(viableFirst, 2);
45730
46146
  const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
45731
- info(MODULE12, `Search complete: ${tracker.getTotalCalls()} REST Search API call(s) + ${getGraphQLSearchQueryCount()} GraphQL broad-search query/queries used, ${finalPicks.length} candidates returned`);
46147
+ info(MODULE13, `Search complete: ${tracker.getTotalCalls()} REST Search API call(s) + ${getGraphQLSearchQueryCount()} GraphQL broad-search query/queries used, ${finalPicks.length} candidates returned`);
45732
46148
  return { candidates: finalPicks, strategiesUsed };
45733
46149
  }
45734
46150
  /**
@@ -45850,7 +46266,7 @@ async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
45850
46266
  }
45851
46267
 
45852
46268
  // ../core/dist/core/feature-discovery.js
45853
- var MODULE13 = "feature-discovery";
46269
+ var MODULE14 = "feature-discovery";
45854
46270
  var INTER_REPO_DELAY_MS = 2e3;
45855
46271
  var MIN_VIABILITY_SCORE = 40;
45856
46272
  var ANCHOR_THRESHOLD = 3;
@@ -45954,7 +46370,7 @@ function isFeatureIssue(item) {
45954
46370
  return false;
45955
46371
  return labels.some((l) => FEATURE_LABELS.includes(l));
45956
46372
  }
45957
- async function vetAndClassify(item, vetter, roadmapRefs) {
46373
+ async function vetAndClassify(item, vetter, minStars, roadmapRefs) {
45958
46374
  const labels = extractLabels(item);
45959
46375
  const hasMilestone = !!item.milestone;
45960
46376
  const reactions = item.reactions?.total_count ?? 0;
@@ -45975,7 +46391,10 @@ async function vetAndClassify(item, vetter, roadmapRefs) {
45975
46391
  });
45976
46392
  } catch (err) {
45977
46393
  rethrowIfFatal(err);
45978
- warn(MODULE13, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
46394
+ warn(MODULE14, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
46395
+ return null;
46396
+ }
46397
+ if (!candidate.projectHealth.checkFailed && (candidate.projectHealth.stargazersCount ?? 0) < minStars) {
45979
46398
  return null;
45980
46399
  }
45981
46400
  const horizon = classifyHorizon({
@@ -46019,12 +46438,12 @@ async function discoverFeatures(opts) {
46019
46438
  roadmapRefs = refs;
46020
46439
  } catch (err) {
46021
46440
  rethrowIfFatal(err);
46022
- warn(MODULE13, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
46441
+ warn(MODULE14, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
46023
46442
  continue;
46024
46443
  }
46025
46444
  const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
46026
46445
  for (const item of items) {
46027
- const candidate = await vetAndClassify(item, opts.vetter, roadmapRefs);
46446
+ const candidate = await vetAndClassify(item, opts.vetter, opts.minStars ?? 0, roadmapRefs);
46028
46447
  if (candidate)
46029
46448
  candidates.push(candidate);
46030
46449
  }
@@ -46101,7 +46520,7 @@ async function discoverFeaturesBroad(opts) {
46101
46520
  }).slice(0, maxToVet);
46102
46521
  } catch (err) {
46103
46522
  rethrowIfFatal(err);
46104
- warn(MODULE13, `broad feature search failed: ${errorMessage(err)}`);
46523
+ warn(MODULE14, `broad feature search failed: ${errorMessage(err)}`);
46105
46524
  return {
46106
46525
  quickWins: [],
46107
46526
  biggerBets: [],
@@ -46111,7 +46530,7 @@ async function discoverFeaturesBroad(opts) {
46111
46530
  }
46112
46531
  const candidates = [];
46113
46532
  for (const item of items) {
46114
- const candidate = await vetAndClassify(item, opts.vetter);
46533
+ const candidate = await vetAndClassify(item, opts.vetter, opts.minStars ?? 0);
46115
46534
  if (candidate)
46116
46535
  candidates.push(candidate);
46117
46536
  }
@@ -46126,9 +46545,58 @@ async function discoverFeaturesBroad(opts) {
46126
46545
  }
46127
46546
 
46128
46547
  // ../core/dist/core/gist-state-store.js
46548
+ var fs5 = __toESM(require("fs"), 1);
46549
+ var path5 = __toESM(require("path"), 1);
46550
+
46551
+ // ../core/dist/core/local-state.js
46129
46552
  var fs4 = __toESM(require("fs"), 1);
46130
46553
  var path4 = __toESM(require("path"), 1);
46131
- var MODULE14 = "gist-state";
46554
+ var MODULE15 = "local-state";
46555
+ function getStatePath() {
46556
+ return path4.join(getDataDir(), "state.json");
46557
+ }
46558
+ function loadLocalState() {
46559
+ const statePath = getStatePath();
46560
+ try {
46561
+ const raw = fs4.readFileSync(statePath, "utf-8");
46562
+ return parseScoutState(JSON.parse(raw));
46563
+ } catch (err) {
46564
+ const code = err?.code;
46565
+ if (code === "ENOENT") {
46566
+ return ScoutStateSchema.parse({ version: 1 });
46567
+ }
46568
+ warn(MODULE15, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
46569
+ try {
46570
+ const backupPath = `${statePath}.corrupt.${Date.now()}`;
46571
+ fs4.copyFileSync(statePath, backupPath);
46572
+ warn(MODULE15, `Corrupt state backed up to ${backupPath}`);
46573
+ } catch (backupErr) {
46574
+ warn(MODULE15, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
46575
+ }
46576
+ return ScoutStateSchema.parse({ version: 1 });
46577
+ }
46578
+ }
46579
+ var tmpCounter = 0;
46580
+ function saveLocalState(state) {
46581
+ writeFileAtomic(getStatePath(), JSON.stringify(state, null, 2) + "\n");
46582
+ debug(MODULE15, "State saved");
46583
+ }
46584
+ function writeFileAtomic(filePath, data) {
46585
+ const tmpPath = `${filePath}.tmp.${process.pid}.${tmpCounter++}`;
46586
+ fs4.writeFileSync(tmpPath, data, { mode: 384 });
46587
+ try {
46588
+ fs4.renameSync(tmpPath, filePath);
46589
+ } catch (err) {
46590
+ try {
46591
+ fs4.unlinkSync(tmpPath);
46592
+ } catch {
46593
+ }
46594
+ throw err;
46595
+ }
46596
+ }
46597
+
46598
+ // ../core/dist/core/gist-state-store.js
46599
+ var MODULE16 = "gist-state";
46132
46600
  var GIST_DESCRIPTION = "oss-scout-state";
46133
46601
  var GIST_FILENAME = "state.json";
46134
46602
  var GIST_ID_FILE = "gist-id";
@@ -46152,10 +46620,10 @@ function classifyDegradedReason(err) {
46152
46620
  return "unknown";
46153
46621
  }
46154
46622
  function getGistIdPath() {
46155
- return path4.join(getDataDir(), GIST_ID_FILE);
46623
+ return path5.join(getDataDir(), GIST_ID_FILE);
46156
46624
  }
46157
46625
  function getCachePath() {
46158
- return path4.join(getDataDir(), CACHE_FILE);
46626
+ return path5.join(getDataDir(), CACHE_FILE);
46159
46627
  }
46160
46628
  var GistStateStore = class {
46161
46629
  octokit;
@@ -46173,7 +46641,7 @@ var GistStateStore = class {
46173
46641
  } catch (err) {
46174
46642
  if (getHttpStatusCode(err) === 401)
46175
46643
  throw err;
46176
- warn(MODULE14, `API bootstrap failed: ${errorMessage(err)}`);
46644
+ warn(MODULE16, `API bootstrap failed: ${errorMessage(err)}`);
46177
46645
  return this.bootstrapFromCache(classifyDegradedReason(err));
46178
46646
  }
46179
46647
  }
@@ -46182,7 +46650,7 @@ var GistStateStore = class {
46182
46650
  */
46183
46651
  async push(state) {
46184
46652
  if (!this.gistId) {
46185
- warn(MODULE14, "No gist ID \u2014 cannot push");
46653
+ warn(MODULE16, "No gist ID \u2014 cannot push");
46186
46654
  this.writeCache(state);
46187
46655
  return false;
46188
46656
  }
@@ -46193,17 +46661,17 @@ var GistStateStore = class {
46193
46661
  toWrite = mergeStates(state, remote.state);
46194
46662
  } else if (remote.kind === "invalid") {
46195
46663
  this.writeCache(state);
46196
- warn(MODULE14, "Remote gist content failed validation \u2014 not overwriting it. Changes saved locally; gist sync skipped.");
46664
+ warn(MODULE16, "Remote gist content failed validation \u2014 not overwriting it. Changes saved locally; gist sync skipped.");
46197
46665
  return false;
46198
46666
  }
46199
46667
  } catch (err) {
46200
46668
  rethrowIfFatal(err);
46201
- warn(MODULE14, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
46669
+ warn(MODULE16, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
46202
46670
  }
46203
46671
  this.writeCache(toWrite);
46204
46672
  const json2 = JSON.stringify(toWrite, null, 2);
46205
46673
  if (json2.length > 9e5) {
46206
- warn(MODULE14, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
46674
+ warn(MODULE16, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
46207
46675
  return false;
46208
46676
  }
46209
46677
  try {
@@ -46213,11 +46681,11 @@ var GistStateStore = class {
46213
46681
  [GIST_FILENAME]: { content: json2 }
46214
46682
  }
46215
46683
  });
46216
- debug(MODULE14, "State pushed to gist");
46684
+ debug(MODULE16, "State pushed to gist");
46217
46685
  return true;
46218
46686
  } catch (err) {
46219
46687
  rethrowIfFatal(err);
46220
- warn(MODULE14, `Failed to push: ${errorMessage(err)}`);
46688
+ warn(MODULE16, `Failed to push: ${errorMessage(err)}`);
46221
46689
  return false;
46222
46690
  }
46223
46691
  }
@@ -46229,7 +46697,7 @@ var GistStateStore = class {
46229
46697
  async bootstrapFromApi() {
46230
46698
  const cachedId = this.readCachedGistId();
46231
46699
  if (cachedId) {
46232
- debug(MODULE14, `Trying cached gist ID: ${cachedId}`);
46700
+ debug(MODULE16, `Trying cached gist ID: ${cachedId}`);
46233
46701
  try {
46234
46702
  const fetched = await this.fetchGistState(cachedId);
46235
46703
  if (fetched.kind === "ok") {
@@ -46241,13 +46709,13 @@ var GistStateStore = class {
46241
46709
  } catch (err) {
46242
46710
  if (getHttpStatusCode(err) !== 404)
46243
46711
  throw err;
46244
- debug(MODULE14, `Cached gist gone (404): ${errorMessage(err)}`);
46712
+ debug(MODULE16, `Cached gist gone (404): ${errorMessage(err)}`);
46245
46713
  }
46246
- debug(MODULE14, "Cached gist ID invalid, searching...");
46714
+ debug(MODULE16, "Cached gist ID invalid, searching...");
46247
46715
  }
46248
46716
  const search = await this.searchForGist();
46249
46717
  if (search.id) {
46250
- debug(MODULE14, `Found gist via search: ${search.id}`);
46718
+ debug(MODULE16, `Found gist via search: ${search.id}`);
46251
46719
  this.saveGistId(search.id);
46252
46720
  this.gistId = search.id;
46253
46721
  const fetched = await this.fetchGistState(search.id);
@@ -46256,13 +46724,13 @@ var GistStateStore = class {
46256
46724
  this.writeCache(state);
46257
46725
  return { gistId: search.id, state, created: false };
46258
46726
  }
46259
- warn(MODULE14, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
46727
+ warn(MODULE16, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
46260
46728
  return this.bootstrapFromCache("unknown");
46261
46729
  }
46262
46730
  if (!search.exhaustive) {
46263
- warn(MODULE14, `Scanned the first ${SEARCH_MAX_PAGES * 100} gists without finding an oss-scout state gist, but the account has more. Creating a new state gist; if one already exists, copy its id into the gist-id file in the oss-scout data directory to avoid a duplicate.`);
46731
+ warn(MODULE16, `Scanned the first ${SEARCH_MAX_PAGES * 100} gists without finding an oss-scout state gist, but the account has more. Creating a new state gist; if one already exists, copy its id into the gist-id file in the oss-scout data directory to avoid a duplicate.`);
46264
46732
  }
46265
- debug(MODULE14, "No existing gist found, creating new one");
46733
+ debug(MODULE16, "No existing gist found, creating new one");
46266
46734
  const freshState = ScoutStateSchema.parse({ version: 1 });
46267
46735
  const newId = await this.createGist(freshState);
46268
46736
  this.saveGistId(newId);
@@ -46273,7 +46741,7 @@ var GistStateStore = class {
46273
46741
  bootstrapFromCache(reason) {
46274
46742
  const cached2 = this.readCache();
46275
46743
  if (cached2) {
46276
- debug(MODULE14, "Bootstrapped from local cache (degraded mode)");
46744
+ debug(MODULE16, "Bootstrapped from local cache (degraded mode)");
46277
46745
  const cachedId = this.readCachedGistId();
46278
46746
  if (cachedId)
46279
46747
  this.gistId = cachedId;
@@ -46285,7 +46753,7 @@ var GistStateStore = class {
46285
46753
  degradedReason: reason
46286
46754
  };
46287
46755
  }
46288
- debug(MODULE14, "No cache available, using fresh state (degraded mode)");
46756
+ debug(MODULE16, "No cache available, using fresh state (degraded mode)");
46289
46757
  const fresh = ScoutStateSchema.parse({ version: 1 });
46290
46758
  return {
46291
46759
  gistId: "",
@@ -46312,7 +46780,7 @@ var GistStateStore = class {
46312
46780
  const parsed = JSON.parse(file2.content);
46313
46781
  return { kind: "ok", state: parseScoutState(parsed) };
46314
46782
  } catch (err) {
46315
- warn(MODULE14, `Gist content failed validation: ${errorMessage(err)}`);
46783
+ warn(MODULE16, `Gist content failed validation: ${errorMessage(err)}`);
46316
46784
  return { kind: "invalid" };
46317
46785
  }
46318
46786
  }
@@ -46350,18 +46818,18 @@ var GistStateStore = class {
46350
46818
  // ── Local file helpers ───────────────────────────────────────────────
46351
46819
  readCachedGistId() {
46352
46820
  try {
46353
- const id = fs4.readFileSync(getGistIdPath(), "utf-8").trim();
46821
+ const id = fs5.readFileSync(getGistIdPath(), "utf-8").trim();
46354
46822
  return id || null;
46355
46823
  } catch (err) {
46356
46824
  const code = err?.code;
46357
46825
  if (code !== "ENOENT") {
46358
- warn(MODULE14, `Failed to read cached gist ID: ${errorMessage(err)}`);
46826
+ warn(MODULE16, `Failed to read cached gist ID: ${errorMessage(err)}`);
46359
46827
  }
46360
46828
  return null;
46361
46829
  }
46362
46830
  }
46363
46831
  saveGistId(id) {
46364
- fs4.writeFileSync(getGistIdPath(), id + "\n", { mode: 384 });
46832
+ writeFileAtomic(getGistIdPath(), id + "\n");
46365
46833
  }
46366
46834
  /**
46367
46835
  * Merge the local state-cache into a freshly fetched gist state before it
@@ -46376,23 +46844,21 @@ var GistStateStore = class {
46376
46844
  }
46377
46845
  readCache() {
46378
46846
  try {
46379
- const raw = fs4.readFileSync(getCachePath(), "utf-8");
46847
+ const raw = fs5.readFileSync(getCachePath(), "utf-8");
46380
46848
  return parseScoutState(JSON.parse(raw));
46381
46849
  } catch (err) {
46382
46850
  const code = err?.code;
46383
46851
  if (code !== "ENOENT") {
46384
- warn(MODULE14, `Failed to read state cache: ${errorMessage(err)}`);
46852
+ warn(MODULE16, `Failed to read state cache: ${errorMessage(err)}`);
46385
46853
  }
46386
46854
  return null;
46387
46855
  }
46388
46856
  }
46389
46857
  writeCache(state) {
46390
46858
  try {
46391
- fs4.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", {
46392
- mode: 384
46393
- });
46859
+ writeFileAtomic(getCachePath(), JSON.stringify(state, null, 2) + "\n");
46394
46860
  } catch (err) {
46395
- warn(MODULE14, `Failed to write cache: ${errorMessage(err)}`);
46861
+ warn(MODULE16, `Failed to write cache: ${errorMessage(err)}`);
46396
46862
  }
46397
46863
  }
46398
46864
  };
@@ -46540,53 +47006,8 @@ function pickFresherTimestamp(a, b) {
46540
47006
  return a >= b ? a : b;
46541
47007
  }
46542
47008
 
46543
- // ../core/dist/core/local-state.js
46544
- var fs5 = __toESM(require("fs"), 1);
46545
- var path5 = __toESM(require("path"), 1);
46546
- var MODULE15 = "local-state";
46547
- function getStatePath() {
46548
- return path5.join(getDataDir(), "state.json");
46549
- }
46550
- function loadLocalState() {
46551
- const statePath = getStatePath();
46552
- try {
46553
- const raw = fs5.readFileSync(statePath, "utf-8");
46554
- return parseScoutState(JSON.parse(raw));
46555
- } catch (err) {
46556
- const code = err?.code;
46557
- if (code === "ENOENT") {
46558
- return ScoutStateSchema.parse({ version: 1 });
46559
- }
46560
- warn(MODULE15, `Failed to load state from ${statePath}: ${errorMessage(err)}. Using defaults.`);
46561
- try {
46562
- const backupPath = `${statePath}.corrupt.${Date.now()}`;
46563
- fs5.copyFileSync(statePath, backupPath);
46564
- warn(MODULE15, `Corrupt state backed up to ${backupPath}`);
46565
- } catch (backupErr) {
46566
- warn(MODULE15, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
46567
- }
46568
- return ScoutStateSchema.parse({ version: 1 });
46569
- }
46570
- }
46571
- var tmpCounter = 0;
46572
- function saveLocalState(state) {
46573
- const statePath = getStatePath();
46574
- const tmpPath = `${statePath}.tmp.${process.pid}.${tmpCounter++}`;
46575
- const data = JSON.stringify(state, null, 2) + "\n";
46576
- fs5.writeFileSync(tmpPath, data, { mode: 384 });
46577
- try {
46578
- fs5.renameSync(tmpPath, statePath);
46579
- } catch (err) {
46580
- try {
46581
- fs5.unlinkSync(tmpPath);
46582
- } catch {
46583
- }
46584
- throw err;
46585
- }
46586
- debug(MODULE15, "State saved");
46587
- }
46588
-
46589
47009
  // ../core/dist/scout.js
47010
+ var LINKED_PR_SKIP_REASON = /Linked PR already merged|Linked PR closed without merge|Has existing PR/;
46590
47011
  function offlineModeMessage(reason) {
46591
47012
  const tail = "Changes will only be saved locally.";
46592
47013
  switch (reason) {
@@ -46696,7 +47117,8 @@ var OssScout = class {
46696
47117
  async search(options) {
46697
47118
  this.evictStaleCacheEntries();
46698
47119
  this.cullExpiredSkips();
46699
- const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
47120
+ const unlockedRepos = new Set(this.getComplexIssuesMode() === "warm" ? this.getReposWithMergedPRs().map((r) => r.toLowerCase()) : []);
47121
+ const skippedUrls = new Set((this.state.skippedIssues ?? []).filter((s) => !(unlockedRepos.has(s.repo.toLowerCase()) && LINKED_PR_SKIP_REASON.test(s.reason ?? ""))).map((s) => s.url));
46700
47122
  if (options?.excludeRecentlySurfaced ?? true) {
46701
47123
  const ttlDays = options?.recentlySurfacedTtlDays ?? 7;
46702
47124
  const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1e3;
@@ -46843,6 +47265,7 @@ var OssScout = class {
46843
47265
  excludeRepos: this.state.preferences.excludeRepos,
46844
47266
  excludeOrgs: this.state.preferences.excludeOrgs,
46845
47267
  aiPolicyBlocklist: this.state.preferences.aiPolicyBlocklist,
47268
+ minStars: this.state.preferences.minStars,
46846
47269
  splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
46847
47270
  }) : await discoverFeatures({
46848
47271
  octokit,
@@ -46852,6 +47275,7 @@ var OssScout = class {
46852
47275
  excludeRepos: this.state.preferences.excludeRepos,
46853
47276
  excludeOrgs: this.state.preferences.excludeOrgs,
46854
47277
  aiPolicyBlocklist: this.state.preferences.aiPolicyBlocklist,
47278
+ minStars: this.state.preferences.minStars,
46855
47279
  anchorThreshold: options?.anchorThreshold ?? this.state.preferences.featuresAnchorThreshold,
46856
47280
  splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
46857
47281
  });
@@ -47025,6 +47449,9 @@ var OssScout = class {
47025
47449
  return null;
47026
47450
  return { model, host: this.state.preferences.slmTriageHost ?? "" };
47027
47451
  }
47452
+ getComplexIssuesMode() {
47453
+ return this.state.preferences.complexIssues ?? "off";
47454
+ }
47028
47455
  /** Get current preferences (read-only). */
47029
47456
  getPreferences() {
47030
47457
  return this.state.preferences;
@@ -47456,6 +47883,7 @@ var FIELD_CONFIGS = {
47456
47883
  boostIssueTypes: { type: "array" },
47457
47884
  slmTriageModel: { type: "string" },
47458
47885
  slmTriageHost: { type: "string" },
47886
+ complexIssues: { type: "enum", validValues: ComplexIssuesModeSchema.options },
47459
47887
  featuresAnchorThreshold: { type: "number" },
47460
47888
  featuresSplitRatio: { type: "float" }
47461
47889
  };
@@ -47948,7 +48376,7 @@ function registerResources(server, scout) {
47948
48376
  // src/index.ts
47949
48377
  var SERVER_INFO = {
47950
48378
  name: "oss-scout-mcp",
47951
- version: "0.13.1"
48379
+ version: "0.13.2"
47952
48380
  // x-release-please-version
47953
48381
  };
47954
48382
  function createServer(scout) {