@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.
- package/dist/cli.bundle.cjs +44 -44
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/dashboard-server.js +15 -5
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.js +62 -0
- package/dist/core/category-mapping.d.ts +19 -0
- package/dist/core/category-mapping.js +58 -0
- package/dist/core/daily-logic.js +7 -2
- package/dist/core/issue-discovery.js +55 -3
- package/dist/core/issue-scoring.d.ts +3 -0
- package/dist/core/issue-scoring.js +5 -0
- package/dist/core/issue-vetting.js +12 -0
- package/dist/core/types.d.ts +8 -1
- package/dist/core/types.js +11 -0
- package/package.json +1 -1
|
@@ -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([
|
|
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
|
|
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 —
|
|
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
|
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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 {
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/daily-logic.js
CHANGED
|
@@ -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:
|
|
300
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/core/types.js
CHANGED
|
@@ -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
|
+
];
|