@oss-autopilot/core 0.49.0 → 0.50.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.
@@ -11,7 +11,7 @@ import * as path from 'path';
11
11
  import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides } from '../core/index.js';
12
12
  import { errorMessage, ValidationError } from '../core/errors.js';
13
13
  import { warn } from '../core/logger.js';
14
- import { validateUrl, validateGitHubUrl, PR_URL_PATTERN } from './validation.js';
14
+ import { validateUrl, validateGitHubUrl, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN } from './validation.js';
15
15
  import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, storedToMergedPRs, storedToClosedPRs, } from './dashboard-data.js';
16
16
  import { openInBrowser } from './startup.js';
17
17
  import { writeDashboardServerInfo, removeDashboardServerInfo } from './dashboard-process.js';
@@ -20,7 +20,12 @@ import { isBelowMinStars, } from '../core/types.js';
20
20
  // Re-export process management functions for backward compatibility
21
21
  export { getDashboardPidPath, writeDashboardServerInfo, readDashboardServerInfo, removeDashboardServerInfo, isDashboardServerRunning, findRunningDashboardServer, } from './dashboard-process.js';
22
22
  // ── Constants ────────────────────────────────────────────────────────────────
23
- const VALID_ACTIONS = new Set(['shelve', 'unshelve', 'override_status']);
23
+ const VALID_ACTIONS = new Set([
24
+ 'shelve',
25
+ 'unshelve',
26
+ 'override_status',
27
+ 'dismiss_issue_response',
28
+ ]);
24
29
  const MODULE = 'dashboard-server';
25
30
  const MAX_BODY_BYTES = 10_240;
26
31
  const REQUEST_TIMEOUT_MS = 30_000;
@@ -51,7 +56,8 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClo
51
56
  const filteredMergedPRs = mergedPRs.filter(isAboveMinStars);
52
57
  const filteredClosedPRs = closedPRs.filter(isAboveMinStars);
53
58
  const stats = buildDashboardStats(digest, state, filteredMergedPRs.length, filteredClosedPRs.length);
54
- const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
59
+ const dismissedIssues = state.config.dismissedIssues || {};
60
+ const issueResponses = commentedIssues.filter((i) => i.status === 'new_response' && !(i.url in dismissedIssues));
55
61
  return {
56
62
  stats,
57
63
  prsByRepo,
@@ -244,10 +250,11 @@ export async function startDashboardServer(options) {
244
250
  sendError(res, 400, 'Missing or invalid "url" field');
245
251
  return;
246
252
  }
247
- // Validate URL format — all actions are PR-only now.
253
+ // Validate URL format — dismiss_issue_response accepts issue or PR URLs, others are PR-only.
254
+ const isDismiss = body.action === 'dismiss_issue_response';
248
255
  try {
249
256
  validateUrl(body.url);
250
- validateGitHubUrl(body.url, PR_URL_PATTERN, 'PR');
257
+ validateGitHubUrl(body.url, isDismiss ? ISSUE_OR_PR_URL_PATTERN : PR_URL_PATTERN, isDismiss ? 'issue or PR' : 'PR');
251
258
  }
252
259
  catch (err) {
253
260
  if (err instanceof ValidationError) {
@@ -283,6 +290,9 @@ export async function startDashboardServer(options) {
283
290
  stateManager.setStatusOverride(body.url, overrideStatus, lastActivityAt);
284
291
  break;
285
292
  }
293
+ case 'dismiss_issue_response':
294
+ stateManager.dismissIssue(body.url, new Date().toISOString());
295
+ break;
286
296
  }
287
297
  stateManager.save();
288
298
  }
@@ -2,6 +2,7 @@
2
2
  * Setup command
3
3
  * Interactive setup / configuration
4
4
  */
5
+ import { type ProjectCategory } from '../core/types.js';
5
6
  interface SetupOptions {
6
7
  reset?: boolean;
7
8
  set?: string[];
@@ -20,6 +21,8 @@ export interface SetupCompleteOutput {
20
21
  approachingDormantDays: number;
21
22
  languages: string[];
22
23
  labels: string[];
24
+ projectCategories: ProjectCategory[];
25
+ preferredOrgs: string[];
23
26
  };
24
27
  }
25
28
  export interface SetupPrompt {
@@ -5,6 +5,7 @@
5
5
  import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
6
6
  import { ValidationError } from '../core/errors.js';
7
7
  import { validateGitHubUsername } from './validation.js';
8
+ import { PROJECT_CATEGORIES } from '../core/types.js';
8
9
  /** Parse and validate a positive integer setting value. */
9
10
  function parsePositiveInt(value, settingName) {
10
11
  const parsed = Number(value);
@@ -111,6 +112,51 @@ export async function runSetup(options) {
111
112
  results[key] = valid.length > 0 ? valid.join(', ') : '(empty)';
112
113
  break;
113
114
  }
115
+ case 'projectCategories': {
116
+ const categories = value
117
+ .split(',')
118
+ .map((c) => c.trim())
119
+ .filter(Boolean);
120
+ const validCategories = [];
121
+ const invalidCategories = [];
122
+ for (const cat of categories) {
123
+ if (PROJECT_CATEGORIES.includes(cat)) {
124
+ validCategories.push(cat);
125
+ }
126
+ else {
127
+ invalidCategories.push(cat);
128
+ }
129
+ }
130
+ if (invalidCategories.length > 0) {
131
+ warnings.push(`Unknown project categories: ${invalidCategories.join(', ')}. Valid: ${PROJECT_CATEGORIES.join(', ')}`);
132
+ }
133
+ const dedupedCategories = [...new Set(validCategories)];
134
+ stateManager.updateConfig({ projectCategories: dedupedCategories });
135
+ results[key] = dedupedCategories.length > 0 ? dedupedCategories.join(', ') : '(empty)';
136
+ break;
137
+ }
138
+ case 'preferredOrgs': {
139
+ const orgs = value
140
+ .split(',')
141
+ .map((o) => o.trim())
142
+ .filter(Boolean);
143
+ const validOrgs = [];
144
+ for (const org of orgs) {
145
+ if (org.includes('/')) {
146
+ warnings.push(`"${org}" looks like a repo path. Use org name only (e.g., "vercel" not "vercel/next.js").`);
147
+ }
148
+ else if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(org)) {
149
+ warnings.push(`"${org}" is not a valid GitHub organization name. Skipping.`);
150
+ }
151
+ else {
152
+ validOrgs.push(org.toLowerCase());
153
+ }
154
+ }
155
+ const dedupedOrgs = [...new Set(validOrgs)];
156
+ stateManager.updateConfig({ preferredOrgs: dedupedOrgs });
157
+ results[key] = dedupedOrgs.length > 0 ? dedupedOrgs.join(', ') : '(empty)';
158
+ break;
159
+ }
114
160
  case 'complete':
115
161
  if (value === 'true') {
116
162
  stateManager.markSetupComplete();
@@ -135,6 +181,8 @@ export async function runSetup(options) {
135
181
  approachingDormantDays: config.approachingDormantDays,
136
182
  languages: config.languages,
137
183
  labels: config.labels,
184
+ projectCategories: config.projectCategories ?? [],
185
+ preferredOrgs: config.preferredOrgs ?? [],
138
186
  },
139
187
  };
140
188
  }
@@ -191,6 +239,20 @@ export async function runSetup(options) {
191
239
  default: ['matplotlib/matplotlib'],
192
240
  type: 'list',
193
241
  },
242
+ {
243
+ setting: 'projectCategories',
244
+ prompt: 'What types of projects interest you? (nonprofit, devtools, infrastructure, web-frameworks, data-ml, education)',
245
+ current: config.projectCategories ?? [],
246
+ default: [],
247
+ type: 'list',
248
+ },
249
+ {
250
+ setting: 'preferredOrgs',
251
+ prompt: 'Any GitHub organizations to prioritize? (org names, comma-separated)',
252
+ current: config.preferredOrgs ?? [],
253
+ default: [],
254
+ type: 'list',
255
+ },
194
256
  ],
195
257
  };
196
258
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Category Mapping — static mappings from project categories to GitHub topics and organizations.
3
+ *
4
+ * Used by issue discovery to prioritize repos matching user's category preferences.
5
+ */
6
+ import type { ProjectCategory } from './types.js';
7
+ /** GitHub topics associated with each project category, used for `topic:` search queries. */
8
+ export declare const CATEGORY_TOPICS: Record<ProjectCategory, string[]>;
9
+ /** Well-known GitHub organizations associated with each project category. */
10
+ export declare const CATEGORY_ORGS: Record<ProjectCategory, string[]>;
11
+ /**
12
+ * Check if a repo belongs to any of the given categories based on its owner matching a category org.
13
+ * Comparison is case-insensitive.
14
+ */
15
+ export declare function repoBelongsToCategory(repoFullName: string, categories: ProjectCategory[]): boolean;
16
+ /**
17
+ * Get deduplicated GitHub topics for the given categories, for use in `topic:` search queries.
18
+ */
19
+ export declare function getTopicsForCategories(categories: ProjectCategory[]): string[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Category Mapping — static mappings from project categories to GitHub topics and organizations.
3
+ *
4
+ * Used by issue discovery to prioritize repos matching user's category preferences.
5
+ */
6
+ /** GitHub topics associated with each project category, used for `topic:` search queries. */
7
+ export const CATEGORY_TOPICS = {
8
+ nonprofit: ['nonprofit', 'social-good', 'humanitarian', 'charity', 'social-impact', 'civic-tech'],
9
+ devtools: ['developer-tools', 'devtools', 'cli', 'sdk', 'linter', 'formatter', 'build-tool'],
10
+ infrastructure: ['infrastructure', 'cloud', 'kubernetes', 'docker', 'devops', 'monitoring', 'observability'],
11
+ 'web-frameworks': ['web-framework', 'frontend', 'backend', 'fullstack', 'nextjs', 'react', 'vue'],
12
+ 'data-ml': ['machine-learning', 'data-science', 'deep-learning', 'nlp', 'data-pipeline', 'analytics'],
13
+ education: ['education', 'learning', 'tutorial', 'courseware', 'edtech', 'teaching'],
14
+ };
15
+ /** Well-known GitHub organizations associated with each project category. */
16
+ export const CATEGORY_ORGS = {
17
+ nonprofit: ['code-for-america', 'opengovfoundation', 'ushahidi', 'hotosm', 'openfn', 'democracyearth'],
18
+ devtools: ['eslint', 'prettier', 'vitejs', 'biomejs', 'oxc-project', 'ast-grep', 'turbot'],
19
+ infrastructure: ['kubernetes', 'hashicorp', 'grafana', 'prometheus', 'open-telemetry', 'envoyproxy', 'cncf'],
20
+ 'web-frameworks': ['vercel', 'remix-run', 'sveltejs', 'nuxt', 'astro', 'redwoodjs', 'blitz-js'],
21
+ 'data-ml': ['huggingface', 'mlflow', 'apache', 'dbt-labs', 'dagster-io', 'prefecthq', 'langchain-ai'],
22
+ education: ['freeCodeCamp', 'TheOdinProject', 'exercism', 'codecademy', 'oppia', 'Khan'],
23
+ };
24
+ /**
25
+ * Check if a repo belongs to any of the given categories based on its owner matching a category org.
26
+ * Comparison is case-insensitive.
27
+ */
28
+ export function repoBelongsToCategory(repoFullName, categories) {
29
+ if (categories.length === 0)
30
+ return false;
31
+ const owner = repoFullName.split('/')[0]?.toLowerCase();
32
+ if (!owner)
33
+ return false;
34
+ for (const category of categories) {
35
+ const orgs = CATEGORY_ORGS[category];
36
+ if (!orgs)
37
+ continue; // Guard against invalid categories from hand-edited state.json
38
+ if (orgs.some((org) => org.toLowerCase() === owner)) {
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ /**
45
+ * Get deduplicated GitHub topics for the given categories, for use in `topic:` search queries.
46
+ */
47
+ export function getTopicsForCategories(categories) {
48
+ const topics = new Set();
49
+ for (const category of categories) {
50
+ const categoryTopics = CATEGORY_TOPICS[category];
51
+ if (!categoryTopics)
52
+ continue; // Guard against invalid categories from hand-edited state.json
53
+ for (const topic of categoryTopics) {
54
+ topics.add(topic);
55
+ }
56
+ }
57
+ return [...topics];
58
+ }
@@ -294,10 +294,15 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
294
294
  const hasActionableIssues = actionableIssues.length > 0;
295
295
  const hasIssueResponses = issueResponses.length > 0;
296
296
  if (hasActionableIssues) {
297
+ const isSingle = actionableIssues.length === 1;
297
298
  items.push({
298
299
  key: 'address_all',
299
- label: `Work through all ${actionableIssues.length} issue${actionableIssues.length === 1 ? '' : 's'} (Recommended)`,
300
- description: 'Run maintenance in parallel, then address code changes one at a time',
300
+ label: isSingle
301
+ ? 'Address this issue (Recommended)'
302
+ : `Work through all ${actionableIssues.length} issues (Recommended)`,
303
+ description: isSingle
304
+ ? 'Fix the issue blocking your PR'
305
+ : 'Run maintenance in parallel, then address code changes one at a time',
301
306
  });
302
307
  }
303
308
  // Issue replies — positioned after address_all but before search
@@ -18,6 +18,7 @@ import { getHttpCache, cachedTimeBased } from './http-cache.js';
18
18
  import { isDocOnlyIssue, detectLabelFarmingRepos, applyPerRepoCap } from './issue-filtering.js';
19
19
  import { IssueVetter } from './issue-vetting.js';
20
20
  import { calculateViabilityScore as calcViabilityScore } from './issue-scoring.js';
21
+ import { getTopicsForCategories } from './category-mapping.js';
21
22
  // Re-export everything from sub-modules for backward compatibility.
22
23
  // Existing consumers (tests, CLI commands) import from './issue-discovery.js'.
23
24
  export { isDocOnlyIssue, applyPerRepoCap, isLabelFarming, hasTemplatedTitle, detectLabelFarmingRepos, DOC_ONLY_LABELS, BEGINNER_LABELS, } from './issue-filtering.js';
@@ -282,6 +283,51 @@ export class IssueDiscovery {
282
283
  }
283
284
  }
284
285
  }
286
+ // Phase 0.5: Search preferred organizations (explicit user preference)
287
+ let phase0_5Error = null;
288
+ const preferredOrgs = config.preferredOrgs ?? [];
289
+ if (allCandidates.length < maxResults && preferredOrgs.length > 0) {
290
+ // Filter out orgs already covered by Phase 0 repos
291
+ const phase0Orgs = new Set(phase0Repos.map((r) => r.split('/')[0]?.toLowerCase()));
292
+ const orgsToSearch = preferredOrgs.filter((org) => !phase0Orgs.has(org.toLowerCase())).slice(0, 5);
293
+ if (orgsToSearch.length > 0) {
294
+ info(MODULE, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
295
+ const remainingNeeded = maxResults - allCandidates.length;
296
+ const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(' OR ');
297
+ const orgQuery = `${baseQuery} (${orgRepoFilter})`;
298
+ try {
299
+ const data = await this.cachedSearch({
300
+ q: orgQuery,
301
+ sort: 'created',
302
+ order: 'desc',
303
+ per_page: remainingNeeded * 3,
304
+ });
305
+ if (data.items.length > 0) {
306
+ const filtered = filterIssues(data.items).filter((item) => {
307
+ const repoFullName = item.repository_url.split('/').slice(-2).join('/');
308
+ return !phase0RepoSet.has(repoFullName);
309
+ });
310
+ const { candidates: orgCandidates, allFailed: allVetFailed, rateLimitHit, } = await this.vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((i) => i.html_url), remainingNeeded, 'preferred_org');
311
+ allCandidates.push(...orgCandidates);
312
+ if (allVetFailed) {
313
+ phase0_5Error = 'All preferred org issue vetting failed';
314
+ }
315
+ if (rateLimitHit) {
316
+ rateLimitHitDuringSearch = true;
317
+ }
318
+ info(MODULE, `Found ${orgCandidates.length} candidates from preferred orgs`);
319
+ }
320
+ }
321
+ catch (error) {
322
+ const errMsg = errorMessage(error);
323
+ phase0_5Error = errMsg;
324
+ if (isRateLimitError(error)) {
325
+ rateLimitHitDuringSearch = true;
326
+ }
327
+ warn(MODULE, `Error searching preferred orgs: ${errMsg}`);
328
+ }
329
+ }
330
+ }
285
331
  // Phase 1: Search starred repos (filter out already-searched Phase 0 repos)
286
332
  if (allCandidates.length < maxResults && starredRepos.length > 0) {
287
333
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
@@ -345,7 +391,12 @@ export class IssueDiscovery {
345
391
  const thirtyDaysAgo = new Date();
346
392
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
347
393
  const pushedSince = thirtyDaysAgo.toISOString().split('T')[0];
348
- const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`
394
+ // When user has category preferences, add a single topic filter to focus on relevant repos.
395
+ // GitHub Search API AND-joins multiple topic: qualifiers, which is overly restrictive,
396
+ // so we pick just the first topic to nudge results without eliminating valid matches.
397
+ const categoryTopics = getTopicsForCategories(config.projectCategories ?? []);
398
+ const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : '';
399
+ const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`
349
400
  .replace(/ +/g, ' ')
350
401
  .trim();
351
402
  try {
@@ -379,6 +430,7 @@ export class IssueDiscovery {
379
430
  if (allCandidates.length === 0) {
380
431
  const phaseErrors = [
381
432
  phase0Error ? `Phase 0 (merged-PR repos): ${phase0Error}` : null,
433
+ phase0_5Error ? `Phase 0.5 (preferred orgs): ${phase0_5Error}` : null,
382
434
  phase1Error ? `Phase 1 (starred repos): ${phase1Error}` : null,
383
435
  phase2Error ? `Phase 2 (general): ${phase2Error}` : null,
384
436
  phase3Error ? `Phase 3 (maintained repos): ${phase3Error}` : null,
@@ -406,8 +458,8 @@ export class IssueDiscovery {
406
458
  }
407
459
  // Sort by priority first, then by recommendation, then by viability score
408
460
  allCandidates.sort((a, b) => {
409
- // Priority order: merged_pr > starred > normal
410
- const priorityOrder = { merged_pr: 0, starred: 1, normal: 2 };
461
+ // Priority order: merged_pr > preferred_org > starred > normal
462
+ const priorityOrder = { merged_pr: 0, preferred_org: 1, starred: 2, normal: 3 };
411
463
  const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
412
464
  if (priorityDiff !== 0)
413
465
  return priorityDiff;
@@ -21,6 +21,8 @@ export interface ViabilityScoreParams {
21
21
  mergedPRCount: number;
22
22
  orgHasMergedPRs: boolean;
23
23
  repoQualityBonus?: number;
24
+ /** True when the repo matches one of the user's preferred project categories. */
25
+ matchesPreferredCategory?: boolean;
24
26
  }
25
27
  /**
26
28
  * Calculate viability score for an issue (0-100 scale)
@@ -33,6 +35,7 @@ export interface ViabilityScoreParams {
33
35
  * - +15 for freshness (recently updated)
34
36
  * - +10 for contribution guidelines
35
37
  * - +5 for org affinity (merged PRs in same org)
38
+ * - +5 for category preference (matches user's project categories)
36
39
  * - -30 if existing PR
37
40
  * - -20 if claimed
38
41
  * - -15 if closed-without-merge history with no merges
@@ -37,6 +37,7 @@ export function calculateRepoQualityBonus(stargazersCount, forksCount) {
37
37
  * - +15 for freshness (recently updated)
38
38
  * - +10 for contribution guidelines
39
39
  * - +5 for org affinity (merged PRs in same org)
40
+ * - +5 for category preference (matches user's project categories)
40
41
  * - -30 if existing PR
41
42
  * - -20 if claimed
42
43
  * - -15 if closed-without-merge history with no merges
@@ -75,6 +76,10 @@ export function calculateViabilityScore(params) {
75
76
  if (params.orgHasMergedPRs) {
76
77
  score += 5;
77
78
  }
79
+ // Category preference bonus (+5) — repo matches user's preferred project categories
80
+ if (params.matchesPreferredCategory) {
81
+ score += 5;
82
+ }
78
83
  // Penalty for existing PR (-30)
79
84
  if (params.hasExistingPR) {
80
85
  score -= 30;
@@ -10,6 +10,7 @@ import { ValidationError, errorMessage, isRateLimitError } from './errors.js';
10
10
  import { warn } from './logger.js';
11
11
  import { getHttpCache, cachedRequest, cachedTimeBased } from './http-cache.js';
12
12
  import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
13
+ import { repoBelongsToCategory } from './category-mapping.js';
13
14
  const MODULE = 'issue-vetting';
14
15
  const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
15
16
  // Cache for contribution guidelines (expires after 1 hour, max 100 entries)
@@ -173,6 +174,12 @@ export class IssueVetter {
173
174
  if (orgHasMergedPRs) {
174
175
  reasonsToApprove.push(`Org affinity (merged PRs in other ${orgName} repos)`);
175
176
  }
177
+ // Check for category preference match
178
+ const projectCategories = config.projectCategories ?? [];
179
+ const matchesCategory = repoBelongsToCategory(repoFullName, projectCategories);
180
+ if (matchesCategory) {
181
+ reasonsToApprove.push('Matches preferred project category');
182
+ }
176
183
  let recommendation;
177
184
  if (vettingResult.passedAllChecks) {
178
185
  recommendation = 'approve';
@@ -207,12 +214,17 @@ export class IssueVetter {
207
214
  mergedPRCount: effectiveMergedCount,
208
215
  orgHasMergedPRs,
209
216
  repoQualityBonus,
217
+ matchesPreferredCategory: matchesCategory,
210
218
  });
211
219
  const starredRepos = this.stateManager.getStarredRepos();
220
+ const preferredOrgs = config.preferredOrgs ?? [];
212
221
  let searchPriority = 'normal';
213
222
  if (effectiveMergedCount > 0) {
214
223
  searchPriority = 'merged_pr';
215
224
  }
225
+ else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
226
+ searchPriority = 'preferred_org';
227
+ }
216
228
  else if (starredRepos.includes(repoFullName)) {
217
229
  searchPriority = 'starred';
218
230
  }
@@ -485,6 +485,10 @@ export interface AgentConfig {
485
485
  snoozedPRs?: Record<string, SnoozeInfo>;
486
486
  /** Manual status overrides for PRs. Maps PR URL to override metadata. Auto-clears when the PR has new activity. */
487
487
  statusOverrides?: Record<string, StatusOverride>;
488
+ /** Project categories the user is interested in (e.g., devtools, nonprofit). Used to prioritize search results. */
489
+ projectCategories?: ProjectCategory[];
490
+ /** GitHub organizations the user wants to prioritize in issue search. Org names only (not owner/repo). */
491
+ preferredOrgs?: string[];
488
492
  }
489
493
  /** Status of a user's comment thread on a GitHub issue. */
490
494
  export type IssueConversationStatus = 'new_response' | 'waiting' | 'acknowledged';
@@ -523,7 +527,10 @@ export type CommentedIssue = CommentedIssueWithResponse | CommentedIssueWithoutR
523
527
  export declare const DEFAULT_CONFIG: AgentConfig;
524
528
  /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v2 architecture. */
525
529
  export declare const INITIAL_STATE: AgentState;
526
- export type SearchPriority = 'merged_pr' | 'starred' | 'normal';
530
+ export declare const PROJECT_CATEGORIES: readonly ["nonprofit", "devtools", "infrastructure", "web-frameworks", "data-ml", "education"];
531
+ export type ProjectCategory = (typeof PROJECT_CATEGORIES)[number];
532
+ /** Priority tier for issue search results. Ordered: merged_pr > preferred_org > starred > normal. */
533
+ export type SearchPriority = 'merged_pr' | 'preferred_org' | 'starred' | 'normal';
527
534
  export interface IssueCandidate {
528
535
  issue: TrackedIssue;
529
536
  vettingResult: IssueVettingResult;
@@ -29,6 +29,8 @@ export const DEFAULT_CONFIG = {
29
29
  shelvedPRUrls: [],
30
30
  dismissedIssues: {},
31
31
  snoozedPRs: {},
32
+ projectCategories: [],
33
+ preferredOrgs: [],
32
34
  };
33
35
  /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v2 architecture. */
34
36
  export const INITIAL_STATE = {
@@ -39,3 +41,12 @@ export const INITIAL_STATE = {
39
41
  events: [],
40
42
  lastRunAt: new Date().toISOString(),
41
43
  };
44
+ // -- Project category types --
45
+ export const PROJECT_CATEGORIES = [
46
+ 'nonprofit',
47
+ 'devtools',
48
+ 'infrastructure',
49
+ 'web-frameworks',
50
+ 'data-ml',
51
+ 'education',
52
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {