@oss-scout/core 0.2.1 → 0.3.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.
package/dist/cli.js CHANGED
@@ -223,7 +223,7 @@ const configCmd = program
223
223
  console.log(formatJsonSuccess(getConfigData()));
224
224
  }
225
225
  else {
226
- runConfigShow(options);
226
+ runConfigShow();
227
227
  }
228
228
  }
229
229
  catch (err) {
@@ -322,6 +322,113 @@ program
322
322
  handleCommandError(err, options);
323
323
  }
324
324
  });
325
+ // ── skip command ───────────────────────────────────────────────────
326
+ const skipCmd = program
327
+ .command("skip")
328
+ .description("Manage the skip list — exclude issues from future searches");
329
+ skipCmd
330
+ .command("add <issue-url>")
331
+ .description("Skip an issue by URL")
332
+ .option("--json", "Output as JSON")
333
+ .action(async (issueUrl, options) => {
334
+ try {
335
+ const { runSkip } = await import("./commands/skip.js");
336
+ const state = loadLocalState();
337
+ const result = await runSkip({ issueUrl, state });
338
+ if (options.json) {
339
+ console.log(formatJsonSuccess(result));
340
+ }
341
+ else {
342
+ if (result.alreadySkipped) {
343
+ console.log("Issue already in skip list.");
344
+ }
345
+ else {
346
+ console.log(`Skipped: ${issueUrl}`);
347
+ }
348
+ }
349
+ }
350
+ catch (err) {
351
+ handleCommandError(err, options);
352
+ }
353
+ });
354
+ skipCmd
355
+ .command("list")
356
+ .description("Show all skipped issues")
357
+ .option("--json", "Output as JSON")
358
+ .action(async (options) => {
359
+ try {
360
+ const { runSkipList } = await import("./commands/skip.js");
361
+ const results = runSkipList();
362
+ if (options.json) {
363
+ console.log(formatJsonSuccess(results));
364
+ }
365
+ else {
366
+ if (results.length === 0) {
367
+ console.log("\nNo skipped issues.\n");
368
+ return;
369
+ }
370
+ console.log(`\nSkipped issues (${results.length}):\n`);
371
+ console.log(" Repo Issue Skipped Title");
372
+ console.log(" ──────────────────────────────── ────── ────────── ─────");
373
+ for (const s of results) {
374
+ const repo = (s.repo || "unknown").padEnd(32).slice(0, 32);
375
+ const issue = s.number ? `#${s.number}`.padEnd(6) : "—".padEnd(6);
376
+ const skippedDate = s.skippedAt.split("T")[0] ?? "";
377
+ const title = s.title.length > 50
378
+ ? s.title.slice(0, 47) + "..."
379
+ : s.title || s.url;
380
+ console.log(` ${repo} ${issue} ${skippedDate} ${title}`);
381
+ }
382
+ console.log();
383
+ }
384
+ }
385
+ catch (err) {
386
+ handleCommandError(err, options);
387
+ }
388
+ });
389
+ skipCmd
390
+ .command("remove <issue-url>")
391
+ .description("Remove an issue from the skip list (unskip)")
392
+ .option("--json", "Output as JSON")
393
+ .action(async (issueUrl, options) => {
394
+ try {
395
+ const { runSkipRemove } = await import("./commands/skip.js");
396
+ const result = await runSkipRemove({ issueUrl });
397
+ if (options.json) {
398
+ console.log(formatJsonSuccess(result));
399
+ }
400
+ else {
401
+ if (result.removed) {
402
+ console.log(`Removed from skip list: ${issueUrl}`);
403
+ }
404
+ else {
405
+ console.log("Issue was not in the skip list.");
406
+ }
407
+ }
408
+ }
409
+ catch (err) {
410
+ handleCommandError(err, options);
411
+ }
412
+ });
413
+ skipCmd
414
+ .command("clear")
415
+ .description("Clear all skipped issues")
416
+ .option("--json", "Output as JSON")
417
+ .action(async (options) => {
418
+ try {
419
+ const { runSkipClear } = await import("./commands/skip.js");
420
+ await runSkipClear();
421
+ if (options.json) {
422
+ console.log(formatJsonSuccess({ cleared: true }));
423
+ }
424
+ else {
425
+ console.log("Skip list cleared.");
426
+ }
427
+ }
428
+ catch (err) {
429
+ handleCommandError(err, options);
430
+ }
431
+ });
325
432
  program
326
433
  .command("vet <issue-url>")
327
434
  .description("Vet a specific GitHub issue for claimability and project health")
@@ -5,9 +5,7 @@ import type { ScoutPreferences } from "../core/schemas.js";
5
5
  /**
6
6
  * Display current preferences in human-readable format.
7
7
  */
8
- export declare function runConfigShow(options: {
9
- json?: boolean;
10
- }): void;
8
+ export declare function runConfigShow(): void;
11
9
  /**
12
10
  * Get current preferences for JSON output.
13
11
  */
@@ -10,7 +10,6 @@ const FIELD_CONFIGS = {
10
10
  excludeRepos: { type: "array" },
11
11
  excludeOrgs: { type: "array" },
12
12
  aiPolicyBlocklist: { type: "array" },
13
- preferredOrgs: { type: "array" },
14
13
  minStars: { type: "number" },
15
14
  maxIssueAgeDays: { type: "number" },
16
15
  minRepoScoreThreshold: { type: "number" },
@@ -73,13 +72,9 @@ function formatArray(arr) {
73
72
  /**
74
73
  * Display current preferences in human-readable format.
75
74
  */
76
- export function runConfigShow(options) {
75
+ export function runConfigShow() {
77
76
  const state = loadLocalState();
78
77
  const prefs = state.preferences;
79
- if (options.json) {
80
- // JSON output handled by caller
81
- return;
82
- }
83
78
  console.log("\n⚙️ oss-scout preferences\n");
84
79
  console.log(` githubUsername: ${prefs.githubUsername || "(not set)"}`);
85
80
  console.log(` languages: ${formatArray(prefs.languages)}`);
@@ -89,7 +84,6 @@ export function runConfigShow(options) {
89
84
  console.log(` maxIssueAgeDays: ${prefs.maxIssueAgeDays}`);
90
85
  console.log(` minRepoScoreThreshold: ${prefs.minRepoScoreThreshold}`);
91
86
  console.log(` includeDocIssues: ${prefs.includeDocIssues}`);
92
- console.log(` preferredOrgs: ${formatArray(prefs.preferredOrgs)}`);
93
87
  console.log(` projectCategories: ${formatArray(prefs.projectCategories)}`);
94
88
  console.log(` excludeRepos: ${formatArray(prefs.excludeRepos)}`);
95
89
  console.log(` excludeOrgs: ${formatArray(prefs.excludeOrgs)}`);
@@ -59,11 +59,9 @@ export async function runSetup(options) {
59
59
  const usernameInput = await ask(rl, usernamePrompt);
60
60
  const githubUsername = usernameInput || usernameDefault;
61
61
  // Languages
62
- const defaultLangs = "typescript, javascript";
63
- const langsInput = await ask(rl, `Preferred languages [${defaultLangs}]: `);
64
- const languages = langsInput
65
- ? parseCSV(langsInput)
66
- : ["typescript", "javascript"];
62
+ const defaultLangs = "any (all languages)";
63
+ const langsInput = await ask(rl, `Preferred languages (comma-separated, or "any" for all) [${defaultLangs}]: `);
64
+ const languages = langsInput ? parseCSV(langsInput) : ["any"];
67
65
  // Issue labels
68
66
  const defaultLabels = "good first issue, help wanted";
69
67
  const labelsInput = await ask(rl, `Issue labels to search for [${defaultLabels}]: `);
@@ -79,9 +77,6 @@ export async function runSetup(options) {
79
77
  // Minimum stars
80
78
  const minStarsInput = await ask(rl, "Minimum repo stars [50]: ");
81
79
  const minStars = minStarsInput ? parseInt(minStarsInput, 10) : 50;
82
- // Preferred organizations
83
- const orgsInput = await ask(rl, "Preferred organizations (comma-separated, optional): ");
84
- const preferredOrgs = parseCSV(orgsInput);
85
80
  // Project categories
86
81
  const categoryOptions = ALL_CATEGORIES.join(", ");
87
82
  const categoriesInput = await ask(rl, `Project categories (${categoryOptions}) [none]: `);
@@ -95,7 +90,6 @@ export async function runSetup(options) {
95
90
  labels,
96
91
  scope: scope.length > 0 ? scope : undefined,
97
92
  excludeRepos,
98
- preferredOrgs,
99
93
  projectCategories,
100
94
  minStars: isNaN(minStars) ? 50 : minStars,
101
95
  });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Skip command — manage the skip list for excluding issues from future searches.
3
+ */
4
+ import type { SkippedIssue, ScoutState } from "../core/schemas.js";
5
+ /**
6
+ * Skip an issue by URL — adds it to the skip list and removes it from saved results.
7
+ * Tries to enrich metadata from saved results if available.
8
+ */
9
+ export declare function runSkip(options: {
10
+ issueUrl: string;
11
+ state?: ScoutState;
12
+ }): Promise<{
13
+ skipped: boolean;
14
+ alreadySkipped: boolean;
15
+ }>;
16
+ /**
17
+ * List all skipped issues.
18
+ */
19
+ export declare function runSkipList(options?: {
20
+ state?: ScoutState;
21
+ }): SkippedIssue[];
22
+ /**
23
+ * Clear all skipped issues.
24
+ */
25
+ export declare function runSkipClear(): Promise<void>;
26
+ /**
27
+ * Remove a specific issue from the skip list (unskip).
28
+ */
29
+ export declare function runSkipRemove(options: {
30
+ issueUrl: string;
31
+ }): Promise<{
32
+ removed: boolean;
33
+ }>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Skip command — manage the skip list for excluding issues from future searches.
3
+ */
4
+ import { loadLocalState, saveLocalState } from "../core/local-state.js";
5
+ import { createScout } from "../scout.js";
6
+ import { getGitHubToken } from "../core/utils.js";
7
+ /**
8
+ * Create an OssScout instance for skip operations.
9
+ * Uses gist persistence when a token is available, otherwise provided-state mode.
10
+ */
11
+ async function createSkipScout(state) {
12
+ const token = getGitHubToken() ?? "";
13
+ if (token) {
14
+ return createScout({
15
+ githubToken: token,
16
+ persistence: "provided",
17
+ initialState: state,
18
+ });
19
+ }
20
+ return createScout({
21
+ githubToken: "",
22
+ persistence: "provided",
23
+ initialState: state,
24
+ });
25
+ }
26
+ /**
27
+ * Skip an issue by URL — adds it to the skip list and removes it from saved results.
28
+ * Tries to enrich metadata from saved results if available.
29
+ */
30
+ export async function runSkip(options) {
31
+ const state = options.state ?? loadLocalState();
32
+ const scout = await createSkipScout(state);
33
+ const alreadySkipped = scout
34
+ .getSkippedIssues()
35
+ .some((s) => s.url === options.issueUrl);
36
+ if (alreadySkipped) {
37
+ return { skipped: false, alreadySkipped: true };
38
+ }
39
+ // Try to enrich metadata from saved results
40
+ const saved = scout
41
+ .getSavedResults()
42
+ .find((r) => r.issueUrl === options.issueUrl);
43
+ const metadata = saved
44
+ ? { repo: saved.repo, number: saved.number, title: saved.title }
45
+ : parseIssueUrl(options.issueUrl);
46
+ scout.skipIssue(options.issueUrl, metadata);
47
+ saveLocalState(scout.getState());
48
+ await scout.checkpoint();
49
+ return { skipped: true, alreadySkipped: false };
50
+ }
51
+ /**
52
+ * List all skipped issues.
53
+ */
54
+ export function runSkipList(options) {
55
+ const state = options?.state ?? loadLocalState();
56
+ return state.skippedIssues ?? [];
57
+ }
58
+ /**
59
+ * Clear all skipped issues.
60
+ */
61
+ export async function runSkipClear() {
62
+ const state = loadLocalState();
63
+ const scout = await createSkipScout(state);
64
+ scout.clearSkippedIssues();
65
+ saveLocalState(scout.getState());
66
+ await scout.checkpoint();
67
+ }
68
+ /**
69
+ * Remove a specific issue from the skip list (unskip).
70
+ */
71
+ export async function runSkipRemove(options) {
72
+ const state = loadLocalState();
73
+ const scout = await createSkipScout(state);
74
+ const before = scout.getSkippedIssues().length;
75
+ scout.unskipIssue(options.issueUrl);
76
+ const removed = before !== scout.getSkippedIssues().length;
77
+ saveLocalState(scout.getState());
78
+ await scout.checkpoint();
79
+ return { removed };
80
+ }
81
+ /**
82
+ * Parse a GitHub issue URL to extract repo and number.
83
+ */
84
+ function parseIssueUrl(url) {
85
+ const match = url.match(/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)/);
86
+ if (!match)
87
+ return undefined;
88
+ return { repo: match[1], number: parseInt(match[2], 10), title: "" };
89
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { getOctokit, checkRateLimit } from "./github.js";
6
6
  import { debug, warn } from "./logger.js";
7
- import { errorMessage } from "./errors.js";
7
+ import { ConfigurationError, errorMessage } from "./errors.js";
8
8
  import { extractRepoFromUrl } from "./utils.js";
9
9
  const MODULE = "bootstrap";
10
10
  const STARRED_MAX_PAGES = 5;
@@ -13,7 +13,7 @@ const PER_PAGE = 100;
13
13
  export async function bootstrapScout(scout, token) {
14
14
  const username = scout.getPreferences().githubUsername;
15
15
  if (!username) {
16
- throw new Error("GitHub username not configured. Run `oss-scout setup` first.");
16
+ throw new ConfigurationError("GitHub username not configured. Run `oss-scout setup` first.");
17
17
  }
18
18
  const rateLimit = await checkRateLimit(token);
19
19
  debug(MODULE, `Rate limit: ${rateLimit.remaining}/${rateLimit.limit}, resets at ${rateLimit.resetAt}`);
@@ -50,11 +50,16 @@ export class GistStateStore {
50
50
  warn(MODULE, "No gist ID — cannot push");
51
51
  return false;
52
52
  }
53
+ const json = JSON.stringify(state, null, 2);
54
+ if (json.length > 900000) {
55
+ warn(MODULE, `State too large for gist (${Math.round(json.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
56
+ return false;
57
+ }
53
58
  try {
54
59
  await this.octokit.gists.update({
55
60
  gist_id: this.gistId,
56
61
  files: {
57
- [GIST_FILENAME]: { content: JSON.stringify(state, null, 2) },
62
+ [GIST_FILENAME]: { content: json },
58
63
  },
59
64
  });
60
65
  debug(MODULE, "State pushed to gist");
@@ -248,6 +253,7 @@ export function mergeStates(local, remote) {
248
253
  mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
249
254
  closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
250
255
  savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
256
+ skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
251
257
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
252
258
  lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
253
259
  new Date().toISOString(),
@@ -302,6 +308,18 @@ function mergeSavedResults(local, remote) {
302
308
  }
303
309
  return [...merged.values()];
304
310
  }
311
+ function mergeSkippedIssues(local, remote) {
312
+ const merged = new Map();
313
+ for (const item of local)
314
+ merged.set(item.url, item);
315
+ for (const item of remote) {
316
+ const existing = merged.get(item.url);
317
+ if (!existing || item.skippedAt > existing.skippedAt) {
318
+ merged.set(item.url, item);
319
+ }
320
+ }
321
+ return [...merged.values()];
322
+ }
305
323
  function pickFresherTimestamp(a, b) {
306
324
  if (!a)
307
325
  return b;
@@ -19,7 +19,6 @@ import { type ScoutStateReader } from "./issue-vetting.js";
19
19
  *
20
20
  * Search phases (in priority order):
21
21
  * 0. Repos where user has merged PRs (highest merge probability)
22
- * 0.5. Preferred organizations
23
22
  * 1. Starred repos
24
23
  * 2. General label-filtered search
25
24
  * 3. Actively maintained repos
@@ -47,8 +46,8 @@ export declare class IssueDiscovery {
47
46
  getStarredRepos(): string[];
48
47
  /**
49
48
  * Search for issues matching our criteria.
50
- * Searches in priority order: merged-PR repos first (no label filter), then preferred
51
- * organizations, then starred repos, then general search, then actively maintained repos.
49
+ * Searches in priority order: merged-PR repos first (no label filter), then starred
50
+ * repos, then general search, then actively maintained repos.
52
51
  * Filters out issues from low-scoring and excluded repos.
53
52
  *
54
53
  * @param options - Search configuration
@@ -74,6 +73,7 @@ export declare class IssueDiscovery {
74
73
  labels?: string[];
75
74
  maxResults?: number;
76
75
  strategies?: SearchStrategy[];
76
+ skippedUrls?: Set<string>;
77
77
  }): Promise<{
78
78
  candidates: IssueCandidate[];
79
79
  strategiesUsed: SearchStrategy[];
@@ -47,6 +47,8 @@ function buildIssueFilter(config) {
47
47
  return false;
48
48
  if (config.lowScoringRepos.has(repoFullName))
49
49
  return false;
50
+ if (config.skippedUrls.has(item.html_url))
51
+ return false;
50
52
  const updatedAt = new Date(item.updated_at);
51
53
  const ageDays = daysBetween(updatedAt, config.now);
52
54
  if (ageDays > config.maxAgeDays)
@@ -68,42 +70,6 @@ async function runPhase0(octokit, vetter, repos, baseQualifiers, maxResults, fil
68
70
  rateLimitHit,
69
71
  };
70
72
  }
71
- /** Phase 0.5: Search preferred organizations. */
72
- async function runPhase05(octokit, vetter, orgsToSearch, baseQualifiers, labels, maxResults, phase0RepoSet, filterIssues) {
73
- info(MODULE, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
74
- const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(" OR ");
75
- const orgOps = orgsToSearch.length - 1;
76
- try {
77
- const allItems = await searchWithChunkedLabels(octokit, labels, orgOps, (labelQ) => `${baseQualifiers} ${labelQ} (${orgRepoFilter})`
78
- .replace(/ +/g, " ")
79
- .trim(), maxResults * 3);
80
- if (allItems.length === 0) {
81
- return { candidates: [], error: null, rateLimitHit: false };
82
- }
83
- const filtered = filterIssues(allItems).filter((item) => {
84
- const repoFullName = extractRepoFromUrl(item.repository_url);
85
- if (!repoFullName)
86
- return false;
87
- return !phase0RepoSet.has(repoFullName);
88
- });
89
- const { candidates, allFailed: allVetFailed, rateLimitHit, } = await vetter.vetIssuesParallel(filtered.slice(0, maxResults * 2).map((i) => i.html_url), maxResults, "preferred_org");
90
- info(MODULE, `Found ${candidates.length} candidates from preferred orgs`);
91
- return {
92
- candidates,
93
- error: allVetFailed ? "All preferred org issue vetting failed" : null,
94
- rateLimitHit,
95
- };
96
- }
97
- catch (error) {
98
- const errMsg = errorMessage(error);
99
- warn(MODULE, `Error searching preferred orgs: ${errMsg}`);
100
- return {
101
- candidates: [],
102
- error: errMsg,
103
- rateLimitHit: isRateLimitError(error),
104
- };
105
- }
106
- }
107
73
  /** Phase 1: Search starred repos. */
108
74
  async function runPhase1(octokit, vetter, repos, baseQualifiers, labels, maxResults, filterIssues) {
109
75
  info(MODULE, `Phase 1: Searching issues in ${repos.length} starred repos...`);
@@ -228,7 +194,6 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
228
194
  *
229
195
  * Search phases (in priority order):
230
196
  * 0. Repos where user has merged PRs (highest merge probability)
231
- * 0.5. Preferred organizations
232
197
  * 1. Starred repos
233
198
  * 2. General label-filtered search
234
199
  * 3. Actively maintained repos
@@ -264,8 +229,8 @@ export class IssueDiscovery {
264
229
  }
265
230
  /**
266
231
  * Search for issues matching our criteria.
267
- * Searches in priority order: merged-PR repos first (no label filter), then preferred
268
- * organizations, then starred repos, then general search, then actively maintained repos.
232
+ * Searches in priority order: merged-PR repos first (no label filter), then starred
233
+ * repos, then general search, then actively maintained repos.
269
234
  * Filters out issues from low-scoring and excluded repos.
270
235
  *
271
236
  * @param options - Search configuration
@@ -329,6 +294,11 @@ export class IssueDiscovery {
329
294
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 60000).toISOString());
330
295
  warn(MODULE, "Could not check rate limit — using conservative budget, skipping heavy phases:", errorMessage(error));
331
296
  }
297
+ if (searchBudget <= 0) {
298
+ this.rateLimitWarning =
299
+ "GitHub search API quota exhausted. Try again after the rate limit resets.";
300
+ return { candidates: [], strategiesUsed: [] };
301
+ }
332
302
  // Derive search context
333
303
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
334
304
  const starredRepos = this.getStarredRepos();
@@ -352,6 +322,7 @@ export class IssueDiscovery {
352
322
  excludeOrgs: new Set((config.excludeOrgs ?? []).map((o) => o.toLowerCase())),
353
323
  aiBlocklisted,
354
324
  lowScoringRepos,
325
+ skippedUrls: options.skippedUrls ?? new Set(),
355
326
  maxAgeDays: config.maxIssueAgeDays || 90,
356
327
  now: new Date(),
357
328
  includeDocIssues: config.includeDocIssues ?? true,
@@ -370,28 +341,6 @@ export class IssueDiscovery {
370
341
  }
371
342
  strategiesUsed.push("merged");
372
343
  }
373
- // Phase 0.5: Preferred organizations
374
- const preferredOrgs = config.preferredOrgs ?? [];
375
- if (allCandidates.length < maxResults &&
376
- preferredOrgs.length > 0 &&
377
- searchBudget >= CRITICAL_BUDGET_THRESHOLD &&
378
- enabledStrategies.has("orgs")) {
379
- if (phase0Repos.length > 0)
380
- await sleep(INTER_PHASE_DELAY_MS);
381
- const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
382
- const orgsToSearch = preferredOrgs
383
- .filter((org) => !phase0Orgs.has(org.toLowerCase()))
384
- .slice(0, 5);
385
- if (orgsToSearch.length > 0) {
386
- const remaining = maxResults - allCandidates.length;
387
- const result = await runPhase05(this.octokit, this.vetter, orgsToSearch, baseQualifiers, labels, remaining, phase0RepoSet, filterIssues);
388
- allCandidates.push(...result.candidates);
389
- phaseErrors["0.5"] = result.error;
390
- if (result.rateLimitHit)
391
- rateLimitHitDuringSearch = true;
392
- }
393
- strategiesUsed.push("orgs");
394
- }
395
344
  // Phase 1: Starred repos
396
345
  if (allCandidates.length < maxResults &&
397
346
  starredRepos.length > 0 &&
@@ -451,9 +400,6 @@ export class IssueDiscovery {
451
400
  phaseErrors["0"]
452
401
  ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}`
453
402
  : null,
454
- phaseErrors["0.5"]
455
- ? `Phase 0.5 (preferred orgs): ${phaseErrors["0.5"]}`
456
- : null,
457
403
  phaseErrors["1"]
458
404
  ? `Phase 1 (starred repos): ${phaseErrors["1"]}`
459
405
  : null,
@@ -482,9 +428,8 @@ export class IssueDiscovery {
482
428
  allCandidates.sort((a, b) => {
483
429
  const priorityOrder = {
484
430
  merged_pr: 0,
485
- preferred_org: 1,
486
- starred: 2,
487
- normal: 3,
431
+ starred: 1,
432
+ normal: 2,
488
433
  };
489
434
  const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
490
435
  if (priorityDiff !== 0)
@@ -17,8 +17,6 @@ export interface ScoutStateReader {
17
17
  getReposWithMergedPRs(): string[];
18
18
  /** User's starred repos (from GitHub). */
19
19
  getStarredRepos(): string[];
20
- /** Preferred GitHub orgs from user preferences. */
21
- getPreferredOrgs(): string[];
22
20
  /** Preferred project categories from user preferences. */
23
21
  getProjectCategories(): ProjectCategory[];
24
22
  /** Numeric quality score for a repo, or null if not evaluated. */
@@ -205,14 +205,10 @@ export class IssueVetter {
205
205
  matchesPreferredCategory: matchesCategory,
206
206
  });
207
207
  const starredRepos = this.stateReader.getStarredRepos();
208
- const preferredOrgs = this.stateReader.getPreferredOrgs();
209
208
  let searchPriority = "normal";
210
209
  if (effectiveMergedCount > 0) {
211
210
  searchPriority = "merged_pr";
212
211
  }
213
- else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
214
- searchPriority = "preferred_org";
215
- }
216
212
  else if (starredRepos.includes(repoFullName)) {
217
213
  searchPriority = "starred";
218
214
  }
@@ -39,8 +39,8 @@ export function loadLocalState() {
39
39
  fs.copyFileSync(statePath, backupPath);
40
40
  warn(MODULE, `Corrupt state backed up to ${backupPath}`);
41
41
  }
42
- catch {
43
- /* best effort backup */
42
+ catch (backupErr) {
43
+ warn(MODULE, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
44
44
  }
45
45
  return ScoutStateSchema.parse({ version: 1 });
46
46
  }
@@ -27,13 +27,12 @@ export declare const IssueScopeSchema: z.ZodEnum<{
27
27
  export declare const SearchStrategySchema: z.ZodEnum<{
28
28
  all: "all";
29
29
  merged: "merged";
30
- orgs: "orgs";
31
30
  starred: "starred";
32
31
  broad: "broad";
33
32
  maintained: "maintained";
34
33
  }>;
35
34
  /** All concrete strategies (excludes 'all' meta-strategy). */
36
- export declare const CONCRETE_STRATEGIES: readonly ["merged", "orgs", "starred", "broad", "maintained"];
35
+ export declare const CONCRETE_STRATEGIES: readonly ["merged", "starred", "broad", "maintained"];
37
36
  export declare const RepoSignalsSchema: z.ZodObject<{
38
37
  hasActiveMaintainers: z.ZodBoolean;
39
38
  isResponsive: z.ZodBoolean;
@@ -152,6 +151,13 @@ export declare const TrackedIssueSchema: z.ZodObject<{
152
151
  notes: z.ZodArray<z.ZodString>;
153
152
  }, z.core.$strip>>;
154
153
  }, z.core.$strip>;
154
+ export declare const SkippedIssueSchema: z.ZodObject<{
155
+ url: z.ZodString;
156
+ repo: z.ZodString;
157
+ number: z.ZodNumber;
158
+ title: z.ZodString;
159
+ skippedAt: z.ZodString;
160
+ }, z.core.$strip>;
155
161
  export declare const SavedCandidateSchema: z.ZodObject<{
156
162
  issueUrl: z.ZodString;
157
163
  repo: z.ZodString;
@@ -185,7 +191,6 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
185
191
  excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
186
192
  excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
187
193
  aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>;
188
- preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
189
194
  projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
190
195
  nonprofit: "nonprofit";
191
196
  devtools: "devtools";
@@ -205,7 +210,6 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
205
210
  defaultStrategy: z.ZodOptional<z.ZodArray<z.ZodEnum<{
206
211
  all: "all";
207
212
  merged: "merged";
208
- orgs: "orgs";
209
213
  starred: "starred";
210
214
  broad: "broad";
211
215
  maintained: "maintained";
@@ -225,7 +229,6 @@ export declare const ScoutStateSchema: z.ZodObject<{
225
229
  excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
226
230
  excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
227
231
  aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>;
228
- preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
229
232
  projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
230
233
  nonprofit: "nonprofit";
231
234
  devtools: "devtools";
@@ -245,7 +248,6 @@ export declare const ScoutStateSchema: z.ZodObject<{
245
248
  defaultStrategy: z.ZodOptional<z.ZodArray<z.ZodEnum<{
246
249
  all: "all";
247
250
  merged: "merged";
248
- orgs: "orgs";
249
251
  starred: "starred";
250
252
  broad: "broad";
251
253
  maintained: "maintained";
@@ -296,11 +298,17 @@ export declare const ScoutStateSchema: z.ZodObject<{
296
298
  lastSeenAt: z.ZodString;
297
299
  lastScore: z.ZodNumber;
298
300
  }, z.core.$strip>>>;
301
+ skippedIssues: z.ZodDefault<z.ZodArray<z.ZodObject<{
302
+ url: z.ZodString;
303
+ repo: z.ZodString;
304
+ number: z.ZodNumber;
305
+ title: z.ZodString;
306
+ skippedAt: z.ZodString;
307
+ }, z.core.$strip>>>;
299
308
  lastSearchAt: z.ZodOptional<z.ZodString>;
300
309
  lastRunAt: z.ZodDefault<z.ZodString>;
301
310
  gistId: z.ZodOptional<z.ZodString>;
302
311
  }, z.core.$strip>;
303
- export type IssueStatus = z.infer<typeof IssueStatusSchema>;
304
312
  export type ProjectCategory = z.infer<typeof ProjectCategorySchema>;
305
313
  export type IssueScope = z.infer<typeof IssueScopeSchema>;
306
314
  export type SearchStrategy = z.infer<typeof SearchStrategySchema>;
@@ -311,7 +319,7 @@ export type StoredClosedPR = z.infer<typeof StoredClosedPRSchema>;
311
319
  export type ContributionGuidelines = z.infer<typeof ContributionGuidelinesSchema>;
312
320
  export type IssueVettingResult = z.infer<typeof IssueVettingResultSchema>;
313
321
  export type TrackedIssue = z.infer<typeof TrackedIssueSchema>;
314
- export type PersistenceMode = z.infer<typeof PersistenceModeSchema>;
315
322
  export type ScoutPreferences = z.infer<typeof ScoutPreferencesSchema>;
316
323
  export type SavedCandidate = z.infer<typeof SavedCandidateSchema>;
324
+ export type SkippedIssue = z.infer<typeof SkippedIssueSchema>;
317
325
  export type ScoutState = z.infer<typeof ScoutStateSchema>;