@oss-autopilot/core 0.42.1 → 0.42.3
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 +804 -751
- package/dist/cli.js +14 -14
- package/dist/commands/daily.d.ts +0 -1
- package/dist/commands/daily.js +4 -3
- package/dist/commands/dashboard-components.d.ts +33 -0
- package/dist/commands/dashboard-components.js +57 -0
- package/dist/commands/dashboard-formatters.d.ts +20 -0
- package/dist/commands/dashboard-formatters.js +33 -0
- package/dist/commands/dashboard-scripts.d.ts +7 -0
- package/dist/commands/dashboard-scripts.js +281 -0
- package/dist/commands/dashboard-styles.d.ts +5 -0
- package/dist/commands/dashboard-styles.js +765 -0
- package/dist/commands/dashboard-templates.d.ts +6 -18
- package/dist/commands/dashboard-templates.js +30 -1134
- package/dist/commands/dismiss.d.ts +4 -6
- package/dist/commands/dismiss.js +11 -13
- package/dist/commands/validation.d.ts +3 -1
- package/dist/commands/validation.js +8 -2
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/issue-discovery.d.ts +5 -0
- package/dist/core/issue-discovery.js +56 -79
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/pr-monitor.js +5 -0
- package/dist/core/review-analysis.js +10 -7
- package/dist/core/state.d.ts +7 -7
- package/dist/core/state.js +7 -7
- package/dist/core/test-utils.d.ts +14 -0
- package/dist/core/test-utils.js +125 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/formatters/json.d.ts +0 -1
- package/package.json +1 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dismiss/Undismiss commands
|
|
3
|
-
* Manages dismissing issue
|
|
4
|
-
* Dismissed
|
|
3
|
+
* Manages dismissing issue and PR notifications without posting a comment.
|
|
4
|
+
* Dismissed URLs resurface automatically when new responses arrive after the dismiss timestamp.
|
|
5
5
|
*/
|
|
6
|
-
import { ISSUE_URL_PATTERN } from './validation.js';
|
|
7
6
|
export interface DismissOutput {
|
|
8
7
|
dismissed: boolean;
|
|
9
8
|
url: string;
|
|
@@ -12,10 +11,9 @@ export interface UndismissOutput {
|
|
|
12
11
|
undismissed: boolean;
|
|
13
12
|
url: string;
|
|
14
13
|
}
|
|
15
|
-
export { ISSUE_URL_PATTERN };
|
|
16
14
|
export declare function runDismiss(options: {
|
|
17
|
-
|
|
15
|
+
url: string;
|
|
18
16
|
}): Promise<DismissOutput>;
|
|
19
17
|
export declare function runUndismiss(options: {
|
|
20
|
-
|
|
18
|
+
url: string;
|
|
21
19
|
}): Promise<UndismissOutput>;
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dismiss/Undismiss commands
|
|
3
|
-
* Manages dismissing issue
|
|
4
|
-
* Dismissed
|
|
3
|
+
* Manages dismissing issue and PR notifications without posting a comment.
|
|
4
|
+
* Dismissed URLs resurface automatically when new responses arrive after the dismiss timestamp.
|
|
5
5
|
*/
|
|
6
6
|
import { getStateManager } from '../core/index.js';
|
|
7
|
-
import {
|
|
8
|
-
// Re-export for backward compatibility with tests
|
|
9
|
-
export { ISSUE_URL_PATTERN };
|
|
7
|
+
import { ISSUE_OR_PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
10
8
|
export async function runDismiss(options) {
|
|
11
|
-
validateUrl(options.
|
|
12
|
-
validateGitHubUrl(options.
|
|
9
|
+
validateUrl(options.url);
|
|
10
|
+
validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, 'issue or PR');
|
|
13
11
|
const stateManager = getStateManager();
|
|
14
|
-
const added = stateManager.dismissIssue(options.
|
|
12
|
+
const added = stateManager.dismissIssue(options.url, new Date().toISOString());
|
|
15
13
|
if (added) {
|
|
16
14
|
stateManager.save();
|
|
17
15
|
}
|
|
18
|
-
return { dismissed: added, url: options.
|
|
16
|
+
return { dismissed: added, url: options.url };
|
|
19
17
|
}
|
|
20
18
|
export async function runUndismiss(options) {
|
|
21
|
-
validateUrl(options.
|
|
22
|
-
validateGitHubUrl(options.
|
|
19
|
+
validateUrl(options.url);
|
|
20
|
+
validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, 'issue or PR');
|
|
23
21
|
const stateManager = getStateManager();
|
|
24
|
-
const removed = stateManager.undismissIssue(options.
|
|
22
|
+
const removed = stateManager.undismissIssue(options.url);
|
|
25
23
|
if (removed) {
|
|
26
24
|
stateManager.save();
|
|
27
25
|
}
|
|
28
|
-
return { undismissed: removed, url: options.
|
|
26
|
+
return { undismissed: removed, url: options.url };
|
|
29
27
|
}
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
export declare const PR_URL_PATTERN: RegExp;
|
|
6
6
|
/** Matches GitHub issue URLs: https://github.com/owner/repo/issues/123 */
|
|
7
7
|
export declare const ISSUE_URL_PATTERN: RegExp;
|
|
8
|
+
/** Matches GitHub issue or PR URLs: /issues/123 or /pull/123 */
|
|
9
|
+
export declare const ISSUE_OR_PR_URL_PATTERN: RegExp;
|
|
8
10
|
/**
|
|
9
11
|
* Validate a GitHub URL against a pattern. Throws if invalid.
|
|
10
12
|
*/
|
|
11
|
-
export declare function validateGitHubUrl(url: string, pattern: RegExp, entityType: 'PR' | 'issue'): void;
|
|
13
|
+
export declare function validateGitHubUrl(url: string, pattern: RegExp, entityType: 'PR' | 'issue' | 'issue or PR'): void;
|
|
12
14
|
/**
|
|
13
15
|
* Validate that a URL does not exceed the maximum allowed length.
|
|
14
16
|
* Returns the URL if valid, throws if too long.
|
|
@@ -6,6 +6,8 @@ import { ValidationError } from '../core/errors.js';
|
|
|
6
6
|
export const PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
|
|
7
7
|
/** Matches GitHub issue URLs: https://github.com/owner/repo/issues/123 */
|
|
8
8
|
export const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
|
|
9
|
+
/** Matches GitHub issue or PR URLs: /issues/123 or /pull/123 */
|
|
10
|
+
export const ISSUE_OR_PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
|
|
9
11
|
/** Maximum allowed URL length */
|
|
10
12
|
const MAX_URL_LENGTH = 2048;
|
|
11
13
|
/** Maximum allowed PR/issue number */
|
|
@@ -20,8 +22,12 @@ const REPO_PATTERN = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
|
|
|
20
22
|
export function validateGitHubUrl(url, pattern, entityType) {
|
|
21
23
|
if (pattern.test(url))
|
|
22
24
|
return;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const examples = {
|
|
26
|
+
PR: 'https://github.com/owner/repo/pull/123',
|
|
27
|
+
issue: 'https://github.com/owner/repo/issues/123',
|
|
28
|
+
'issue or PR': 'https://github.com/owner/repo/issues/123 or https://github.com/owner/repo/pull/123',
|
|
29
|
+
};
|
|
30
|
+
throw new ValidationError(`Invalid ${entityType} URL: ${url}. Expected format: ${examples[entityType]}`);
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
33
|
* Validate that a URL does not exceed the maximum allowed length.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
|
10
10
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
11
11
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
13
|
-
export { enableDebug, isDebugEnabled, debug, warn, timed } from './logger.js';
|
|
13
|
+
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, resetHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
16
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
|
10
10
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
11
11
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
13
|
-
export { enableDebug, isDebugEnabled, debug, warn, timed } from './logger.js';
|
|
13
|
+
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, resetHttpCache, cachedRequest } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
16
|
export * from './types.js';
|
|
@@ -29,6 +29,11 @@ export declare class IssueDiscovery {
|
|
|
29
29
|
* Get starred repos, fetching from GitHub if cache is stale
|
|
30
30
|
*/
|
|
31
31
|
getStarredReposWithRefresh(): Promise<string[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Shared pipeline for Phases 2 and 3: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
34
|
+
* Extracts the common logic so each phase only needs to supply search results and context.
|
|
35
|
+
*/
|
|
36
|
+
private filterVetAndScore;
|
|
32
37
|
/**
|
|
33
38
|
* Search for issues matching our criteria.
|
|
34
39
|
* Searches in priority order: merged-PR repos first (no label filter), then starred repos,
|
|
@@ -13,7 +13,7 @@ import { getStateManager } from './state.js';
|
|
|
13
13
|
import { daysBetween, getDataDir } from './utils.js';
|
|
14
14
|
import { DEFAULT_CONFIG } from './types.js';
|
|
15
15
|
import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
16
|
-
import { warn } from './logger.js';
|
|
16
|
+
import { debug, info, warn } from './logger.js';
|
|
17
17
|
import { isDocOnlyIssue, detectLabelFarmingRepos, applyPerRepoCap } from './issue-filtering.js';
|
|
18
18
|
import { IssueVetter } from './issue-vetting.js';
|
|
19
19
|
import { calculateViabilityScore as calcViabilityScore } from './issue-scoring.js';
|
|
@@ -40,7 +40,7 @@ export class IssueDiscovery {
|
|
|
40
40
|
* Updates the state manager with the list and timestamp.
|
|
41
41
|
*/
|
|
42
42
|
async fetchStarredRepos() {
|
|
43
|
-
|
|
43
|
+
info(MODULE, 'Fetching starred repositories...');
|
|
44
44
|
const starredRepos = [];
|
|
45
45
|
try {
|
|
46
46
|
// Paginate through all starred repos (up to 500 to avoid excessive API calls)
|
|
@@ -68,11 +68,11 @@ export class IssueDiscovery {
|
|
|
68
68
|
pageCount++;
|
|
69
69
|
// Limit to 5 pages (500 repos) to avoid excessive API usage
|
|
70
70
|
if (pageCount >= 5) {
|
|
71
|
-
|
|
71
|
+
info(MODULE, 'Reached pagination limit for starred repos (500)');
|
|
72
72
|
break;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
info(MODULE, `Fetched ${starredRepos.length} starred repositories`);
|
|
76
76
|
this.stateManager.setStarredRepos(starredRepos);
|
|
77
77
|
return starredRepos;
|
|
78
78
|
}
|
|
@@ -101,6 +101,41 @@ export class IssueDiscovery {
|
|
|
101
101
|
}
|
|
102
102
|
return this.stateManager.getStarredRepos();
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Shared pipeline for Phases 2 and 3: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
106
|
+
* Extracts the common logic so each phase only needs to supply search results and context.
|
|
107
|
+
*/
|
|
108
|
+
async filterVetAndScore(items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
|
|
109
|
+
const spamRepos = detectLabelFarmingRepos(items);
|
|
110
|
+
if (spamRepos.size > 0) {
|
|
111
|
+
const spamCount = items.filter((i) => spamRepos.has(i.repository_url.split('/').slice(-2).join('/'))).length;
|
|
112
|
+
debug(MODULE, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
const itemsToVet = filterIssues(items)
|
|
115
|
+
.filter((item) => {
|
|
116
|
+
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
117
|
+
if (spamRepos.has(repoFullName))
|
|
118
|
+
return false;
|
|
119
|
+
return excludedRepoSets.every((s) => !s.has(repoFullName));
|
|
120
|
+
})
|
|
121
|
+
.slice(0, remainingNeeded * 2);
|
|
122
|
+
if (itemsToVet.length === 0) {
|
|
123
|
+
debug(MODULE, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
|
|
124
|
+
return { candidates: [], allVetFailed: false, rateLimitHit: false };
|
|
125
|
+
}
|
|
126
|
+
const { candidates: results, allFailed: allVetFailed, rateLimitHit, } = await this.vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, 'normal');
|
|
127
|
+
const starFiltered = results.filter((c) => {
|
|
128
|
+
if (c.projectHealth.checkFailed)
|
|
129
|
+
return true;
|
|
130
|
+
const stars = c.projectHealth.stargazersCount ?? 0;
|
|
131
|
+
return stars >= minStars;
|
|
132
|
+
});
|
|
133
|
+
const starFilteredCount = results.length - starFiltered.length;
|
|
134
|
+
if (starFilteredCount > 0) {
|
|
135
|
+
debug(MODULE, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
|
|
136
|
+
}
|
|
137
|
+
return { candidates: starFiltered, allVetFailed, rateLimitHit };
|
|
138
|
+
}
|
|
104
139
|
/**
|
|
105
140
|
* Search for issues matching our criteria.
|
|
106
141
|
* Searches in priority order: merged-PR repos first (no label filter), then starred repos,
|
|
@@ -112,6 +147,7 @@ export class IssueDiscovery {
|
|
|
112
147
|
const languages = options.languages || config.languages;
|
|
113
148
|
const labels = options.labels || config.labels;
|
|
114
149
|
const maxResults = options.maxResults || 10;
|
|
150
|
+
const minStars = config.minStars ?? 50;
|
|
115
151
|
const allCandidates = [];
|
|
116
152
|
let phase0Error = null;
|
|
117
153
|
let phase1Error = null;
|
|
@@ -162,7 +198,7 @@ export class IssueDiscovery {
|
|
|
162
198
|
const includeDocIssues = config.includeDocIssues ?? true;
|
|
163
199
|
const aiBlocklisted = new Set(config.aiPolicyBlocklist ?? DEFAULT_CONFIG.aiPolicyBlocklist ?? []);
|
|
164
200
|
if (aiBlocklisted.size > 0) {
|
|
165
|
-
|
|
201
|
+
debug(MODULE, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(', ')}`);
|
|
166
202
|
}
|
|
167
203
|
const filterIssues = (items) => {
|
|
168
204
|
return items.filter((item) => {
|
|
@@ -196,7 +232,7 @@ export class IssueDiscovery {
|
|
|
196
232
|
if (phase0Repos.length > 0) {
|
|
197
233
|
const mergedInPhase0 = Math.min(mergedPRRepos.length, phase0Repos.length);
|
|
198
234
|
const openInPhase0 = phase0Repos.length - mergedInPhase0;
|
|
199
|
-
|
|
235
|
+
info(MODULE, `Phase 0: Searching issues in ${phase0Repos.length} repos (${mergedInPhase0} merged-PR, ${openInPhase0} open-PR, no label filter)...`);
|
|
200
236
|
// Phase 0a: merged-PR repos (priority: merged_pr)
|
|
201
237
|
const mergedPhase0Repos = phase0Repos.slice(0, mergedInPhase0);
|
|
202
238
|
if (mergedPhase0Repos.length > 0) {
|
|
@@ -210,7 +246,7 @@ export class IssueDiscovery {
|
|
|
210
246
|
if (rateLimitHit) {
|
|
211
247
|
rateLimitHitDuringSearch = true;
|
|
212
248
|
}
|
|
213
|
-
|
|
249
|
+
info(MODULE, `Found ${mergedCandidates.length} candidates from merged-PR repos`);
|
|
214
250
|
}
|
|
215
251
|
}
|
|
216
252
|
// Phase 0b: open-PR repos (priority: starred — intermediate tier)
|
|
@@ -227,7 +263,7 @@ export class IssueDiscovery {
|
|
|
227
263
|
if (rateLimitHit) {
|
|
228
264
|
rateLimitHitDuringSearch = true;
|
|
229
265
|
}
|
|
230
|
-
|
|
266
|
+
info(MODULE, `Found ${openCandidates.length} candidates from open-PR repos`);
|
|
231
267
|
}
|
|
232
268
|
}
|
|
233
269
|
}
|
|
@@ -235,7 +271,7 @@ export class IssueDiscovery {
|
|
|
235
271
|
if (allCandidates.length < maxResults && starredRepos.length > 0) {
|
|
236
272
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
237
273
|
if (reposToSearch.length > 0) {
|
|
238
|
-
|
|
274
|
+
info(MODULE, `Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
|
|
239
275
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
240
276
|
if (remainingNeeded > 0) {
|
|
241
277
|
const { candidates: starredCandidates, allBatchesFailed, rateLimitHit, } = await this.searchInRepos(reposToSearch.slice(0, 10), baseQuery, remainingNeeded, 'starred', filterIssues);
|
|
@@ -246,14 +282,14 @@ export class IssueDiscovery {
|
|
|
246
282
|
if (rateLimitHit) {
|
|
247
283
|
rateLimitHitDuringSearch = true;
|
|
248
284
|
}
|
|
249
|
-
|
|
285
|
+
info(MODULE, `Found ${starredCandidates.length} candidates from starred repos`);
|
|
250
286
|
}
|
|
251
287
|
}
|
|
252
288
|
}
|
|
253
289
|
// Phase 2: General search (if still need more)
|
|
254
290
|
let phase2Error = null;
|
|
255
291
|
if (allCandidates.length < maxResults) {
|
|
256
|
-
|
|
292
|
+
info(MODULE, 'Phase 2: General issue search...');
|
|
257
293
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
258
294
|
try {
|
|
259
295
|
const { data } = await this.octokit.search.issuesAndPullRequests({
|
|
@@ -262,40 +298,9 @@ export class IssueDiscovery {
|
|
|
262
298
|
order: 'desc',
|
|
263
299
|
per_page: remainingNeeded * 3, // Fetch extra since some will be filtered
|
|
264
300
|
});
|
|
265
|
-
|
|
266
|
-
// Detect and filter label-farming repos (#97)
|
|
267
|
-
const spamRepos = detectLabelFarmingRepos(data.items);
|
|
268
|
-
if (spamRepos.size > 0) {
|
|
269
|
-
const spamCount = data.items.filter((i) => spamRepos.has(i.repository_url.split('/').slice(-2).join('/'))).length;
|
|
270
|
-
console.log(`[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(', ')}`);
|
|
271
|
-
}
|
|
272
|
-
// Filter and exclude already-found repos
|
|
301
|
+
info(MODULE, `Found ${data.total_count} issues in general search, processing top ${data.items.length}...`);
|
|
273
302
|
const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
|
|
274
|
-
const
|
|
275
|
-
.filter((item) => {
|
|
276
|
-
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
277
|
-
return !spamRepos.has(repoFullName);
|
|
278
|
-
})
|
|
279
|
-
.filter((item) => {
|
|
280
|
-
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
281
|
-
// Skip if already searched in earlier phases
|
|
282
|
-
return (!phase0RepoSet.has(repoFullName) && !starredRepoSet.has(repoFullName) && !seenRepos.has(repoFullName));
|
|
283
|
-
})
|
|
284
|
-
.slice(0, remainingNeeded * 2);
|
|
285
|
-
const { candidates: results, allFailed: allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, 'normal');
|
|
286
|
-
// Apply minStars filter to Phase 2 results (#105)
|
|
287
|
-
// Phase 0/1 are exempt — if you've already contributed to or starred a repo, star count is irrelevant.
|
|
288
|
-
const minStars = config.minStars ?? 50;
|
|
289
|
-
const starFiltered = results.filter((c) => {
|
|
290
|
-
if (c.projectHealth.checkFailed)
|
|
291
|
-
return true; // Don't penalize repos we couldn't check
|
|
292
|
-
const stars = c.projectHealth.stargazersCount ?? 0;
|
|
293
|
-
return stars >= minStars;
|
|
294
|
-
});
|
|
295
|
-
const starFilteredCount = results.length - starFiltered.length;
|
|
296
|
-
if (starFilteredCount > 0) {
|
|
297
|
-
console.log(`[STAR_FILTER] Filtered ${starFilteredCount} candidates below ${minStars} stars`);
|
|
298
|
-
}
|
|
303
|
+
const { candidates: starFiltered, allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.filterVetAndScore(data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], remainingNeeded, minStars, 'Phase 2');
|
|
299
304
|
allCandidates.push(...starFiltered);
|
|
300
305
|
if (allVetFailed) {
|
|
301
306
|
phase2Error = (phase2Error ? phase2Error + '; ' : '') + 'all vetting failed';
|
|
@@ -303,7 +308,7 @@ export class IssueDiscovery {
|
|
|
303
308
|
if (vetRateLimitHit) {
|
|
304
309
|
rateLimitHitDuringSearch = true;
|
|
305
310
|
}
|
|
306
|
-
|
|
311
|
+
info(MODULE, `Found ${starFiltered.length} candidates from general search`);
|
|
307
312
|
}
|
|
308
313
|
catch (error) {
|
|
309
314
|
const errMsg = errorMessage(error);
|
|
@@ -320,13 +325,12 @@ export class IssueDiscovery {
|
|
|
320
325
|
// Uses label-free query to cast a wider net focused on repo health.
|
|
321
326
|
let phase3Error = null;
|
|
322
327
|
if (allCandidates.length < maxResults) {
|
|
323
|
-
|
|
328
|
+
info(MODULE, 'Phase 3: Searching actively maintained repos...');
|
|
324
329
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
325
330
|
const thirtyDaysAgo = new Date();
|
|
326
331
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
327
332
|
const pushedSince = thirtyDaysAgo.toISOString().split('T')[0];
|
|
328
|
-
const
|
|
329
|
-
const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${phase3MinStars} pushed:>=${pushedSince} archived:false`
|
|
333
|
+
const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`
|
|
330
334
|
.replace(/ +/g, ' ')
|
|
331
335
|
.trim();
|
|
332
336
|
try {
|
|
@@ -336,36 +340,9 @@ export class IssueDiscovery {
|
|
|
336
340
|
order: 'desc',
|
|
337
341
|
per_page: remainingNeeded * 3,
|
|
338
342
|
});
|
|
339
|
-
|
|
340
|
-
// Filter spam, already-found repos, and already-searched repos
|
|
341
|
-
const spamRepos = detectLabelFarmingRepos(data.items);
|
|
342
|
-
if (spamRepos.size > 0) {
|
|
343
|
-
const spamCount = data.items.filter((i) => spamRepos.has(i.repository_url.split('/').slice(-2).join('/'))).length;
|
|
344
|
-
console.log(`[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(', ')}`);
|
|
345
|
-
}
|
|
343
|
+
info(MODULE, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
346
344
|
const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
|
|
347
|
-
const
|
|
348
|
-
.filter((item) => {
|
|
349
|
-
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
350
|
-
return (!spamRepos.has(repoFullName) &&
|
|
351
|
-
!phase0RepoSet.has(repoFullName) &&
|
|
352
|
-
!starredRepoSet.has(repoFullName) &&
|
|
353
|
-
!seenRepos.has(repoFullName));
|
|
354
|
-
})
|
|
355
|
-
.slice(0, remainingNeeded * 2);
|
|
356
|
-
const { candidates: results, allFailed: allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, 'normal');
|
|
357
|
-
// Apply minStars filter (same as Phase 2, #105)
|
|
358
|
-
const minStars = config.minStars ?? 50;
|
|
359
|
-
const starFiltered = results.filter((c) => {
|
|
360
|
-
if (c.projectHealth.checkFailed)
|
|
361
|
-
return true;
|
|
362
|
-
const stars = c.projectHealth.stargazersCount ?? 0;
|
|
363
|
-
return stars >= minStars;
|
|
364
|
-
});
|
|
365
|
-
const starFilteredCount = results.length - starFiltered.length;
|
|
366
|
-
if (starFilteredCount > 0) {
|
|
367
|
-
console.log(`[STAR_FILTER] Filtered ${starFilteredCount} Phase 3 candidates below ${minStars} stars`);
|
|
368
|
-
}
|
|
345
|
+
const { candidates: starFiltered, allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.filterVetAndScore(data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], remainingNeeded, minStars, 'Phase 3');
|
|
369
346
|
allCandidates.push(...starFiltered);
|
|
370
347
|
if (allVetFailed) {
|
|
371
348
|
phase3Error = 'all vetting failed';
|
|
@@ -373,7 +350,7 @@ export class IssueDiscovery {
|
|
|
373
350
|
if (vetRateLimitHit) {
|
|
374
351
|
rateLimitHitDuringSearch = true;
|
|
375
352
|
}
|
|
376
|
-
|
|
353
|
+
info(MODULE, `Found ${starFiltered.length} candidates from maintained-repo search`);
|
|
377
354
|
}
|
|
378
355
|
catch (error) {
|
|
379
356
|
const errMsg = errorMessage(error);
|
|
@@ -553,7 +530,7 @@ export class IssueDiscovery {
|
|
|
553
530
|
content += `- **Score**: Viability score (0-100)\n`;
|
|
554
531
|
content += `- **Recommendation**: Y = approve, N = skip, ? = needs_review\n`;
|
|
555
532
|
fs.writeFileSync(outputFile, content, 'utf-8');
|
|
556
|
-
|
|
533
|
+
info(MODULE, `Saved ${sorted.length} issues to ${outputFile}`);
|
|
557
534
|
return outputFile;
|
|
558
535
|
}
|
|
559
536
|
/**
|
package/dist/core/logger.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export declare function isDebugEnabled(): boolean;
|
|
|
11
11
|
* Log a debug message. Only outputs when --debug is enabled.
|
|
12
12
|
*/
|
|
13
13
|
export declare function debug(module: string, message: string, ...args: unknown[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* Log an informational message. Always outputs to stderr.
|
|
16
|
+
* Use for user-facing progress indicators during long-running operations.
|
|
17
|
+
*/
|
|
18
|
+
export declare function info(module: string, message: string, ...args: unknown[]): void;
|
|
14
19
|
/**
|
|
15
20
|
* Log a warning. Always outputs.
|
|
16
21
|
*/
|
package/dist/core/logger.js
CHANGED
|
@@ -21,6 +21,14 @@ export function debug(module, message, ...args) {
|
|
|
21
21
|
const timestamp = new Date().toISOString();
|
|
22
22
|
console.error(`[${timestamp}] [DEBUG] [${module}] ${message}`, ...args);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Log an informational message. Always outputs to stderr.
|
|
26
|
+
* Use for user-facing progress indicators during long-running operations.
|
|
27
|
+
*/
|
|
28
|
+
export function info(module, message, ...args) {
|
|
29
|
+
const timestamp = new Date().toISOString();
|
|
30
|
+
console.error(`[${timestamp}] [INFO] [${module}] ${message}`, ...args);
|
|
31
|
+
}
|
|
24
32
|
/**
|
|
25
33
|
* Log a warning. Always outputs.
|
|
26
34
|
*/
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -283,6 +283,11 @@ export class PRMonitor {
|
|
|
283
283
|
// If the contributor pushed a commit after the maintainer's comment,
|
|
284
284
|
// the changes have been addressed — waiting for maintainer re-review
|
|
285
285
|
if (latestCommitDate && lastMaintainerCommentDate && latestCommitDate > lastMaintainerCommentDate) {
|
|
286
|
+
// Safety net (#431): if a CHANGES_REQUESTED review was submitted after
|
|
287
|
+
// the commit, the maintainer still expects changes — don't mask it
|
|
288
|
+
if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
|
|
289
|
+
return 'needs_response';
|
|
290
|
+
}
|
|
286
291
|
if (ciStatus === 'failing')
|
|
287
292
|
return 'failing_ci';
|
|
288
293
|
return 'changes_addressed';
|
|
@@ -101,14 +101,15 @@ export function checkUnrespondedComments(comments, reviews, reviewComments, user
|
|
|
101
101
|
if (!review.submitted_at)
|
|
102
102
|
continue;
|
|
103
103
|
const body = (review.body || '').trim();
|
|
104
|
-
// Include COMMENTED reviews even without body text —
|
|
105
|
-
// inline review comments were posted and
|
|
106
|
-
//
|
|
107
|
-
// as those are state changes
|
|
108
|
-
if (!body && review.state !== 'COMMENTED')
|
|
104
|
+
// Include COMMENTED and CHANGES_REQUESTED reviews even without body text —
|
|
105
|
+
// they indicate inline review comments were posted and need a response (#151, #431).
|
|
106
|
+
// CHANGES_REQUESTED with only inline comments is actionable maintainer feedback.
|
|
107
|
+
// Skip other empty-body reviews (APPROVED, DISMISSED) as those are state changes.
|
|
108
|
+
if (!body && review.state !== 'COMMENTED' && review.state !== 'CHANGES_REQUESTED')
|
|
109
109
|
continue;
|
|
110
110
|
const author = review.user?.login || 'unknown';
|
|
111
|
-
// For inline-only COMMENTED reviews, skip pure self-replies (#199)
|
|
111
|
+
// For inline-only COMMENTED reviews, skip pure self-replies (#199).
|
|
112
|
+
// CHANGES_REQUESTED reviews are always actionable regardless of self-replies.
|
|
112
113
|
if (!body && review.state === 'COMMENTED' && review.id != null) {
|
|
113
114
|
if (isAllSelfReplies(review.id, reviewComments)) {
|
|
114
115
|
continue;
|
|
@@ -117,7 +118,9 @@ export function checkUnrespondedComments(comments, reviews, reviewComments, user
|
|
|
117
118
|
// Resolve body: prefer actual text, then inline comment text, then synthetic placeholder
|
|
118
119
|
const resolvedBody = body ||
|
|
119
120
|
(review.id != null ? getInlineCommentBody(review.id, reviewComments) : undefined) ||
|
|
120
|
-
|
|
121
|
+
(review.state === 'CHANGES_REQUESTED'
|
|
122
|
+
? '(requested changes via inline review comments)'
|
|
123
|
+
: '(posted inline review comments)');
|
|
121
124
|
timeline.push({
|
|
122
125
|
author,
|
|
123
126
|
body: resolvedBody,
|
package/dist/core/state.d.ts
CHANGED
|
@@ -206,22 +206,22 @@ export declare class StateManager {
|
|
|
206
206
|
*/
|
|
207
207
|
isPRShelved(url: string): boolean;
|
|
208
208
|
/**
|
|
209
|
-
* Dismiss an issue by URL. Dismissed
|
|
209
|
+
* Dismiss an issue or PR by URL. Dismissed URLs are excluded from `new_response` notifications
|
|
210
210
|
* until new activity occurs after the dismiss timestamp.
|
|
211
|
-
* @param url - The full GitHub issue URL.
|
|
212
|
-
* @param timestamp - ISO timestamp of when the issue was dismissed.
|
|
211
|
+
* @param url - The full GitHub issue or PR URL.
|
|
212
|
+
* @param timestamp - ISO timestamp of when the issue/PR was dismissed.
|
|
213
213
|
* @returns true if newly dismissed, false if already dismissed.
|
|
214
214
|
*/
|
|
215
215
|
dismissIssue(url: string, timestamp: string): boolean;
|
|
216
216
|
/**
|
|
217
|
-
* Undismiss an issue by URL.
|
|
218
|
-
* @param url - The full GitHub issue URL.
|
|
217
|
+
* Undismiss an issue or PR by URL.
|
|
218
|
+
* @param url - The full GitHub issue or PR URL.
|
|
219
219
|
* @returns true if found and removed, false if not dismissed.
|
|
220
220
|
*/
|
|
221
221
|
undismissIssue(url: string): boolean;
|
|
222
222
|
/**
|
|
223
|
-
* Get the timestamp when an issue was dismissed.
|
|
224
|
-
* @param url - The full GitHub issue URL.
|
|
223
|
+
* Get the timestamp when an issue or PR was dismissed.
|
|
224
|
+
* @param url - The full GitHub issue or PR URL.
|
|
225
225
|
* @returns The ISO dismiss timestamp, or undefined if not dismissed.
|
|
226
226
|
*/
|
|
227
227
|
getIssueDismissedAt(url: string): string | undefined;
|
package/dist/core/state.js
CHANGED
|
@@ -721,10 +721,10 @@ export class StateManager {
|
|
|
721
721
|
}
|
|
722
722
|
// === Dismiss / Undismiss Issues ===
|
|
723
723
|
/**
|
|
724
|
-
* Dismiss an issue by URL. Dismissed
|
|
724
|
+
* Dismiss an issue or PR by URL. Dismissed URLs are excluded from `new_response` notifications
|
|
725
725
|
* until new activity occurs after the dismiss timestamp.
|
|
726
|
-
* @param url - The full GitHub issue URL.
|
|
727
|
-
* @param timestamp - ISO timestamp of when the issue was dismissed.
|
|
726
|
+
* @param url - The full GitHub issue or PR URL.
|
|
727
|
+
* @param timestamp - ISO timestamp of when the issue/PR was dismissed.
|
|
728
728
|
* @returns true if newly dismissed, false if already dismissed.
|
|
729
729
|
*/
|
|
730
730
|
dismissIssue(url, timestamp) {
|
|
@@ -738,8 +738,8 @@ export class StateManager {
|
|
|
738
738
|
return true;
|
|
739
739
|
}
|
|
740
740
|
/**
|
|
741
|
-
* Undismiss an issue by URL.
|
|
742
|
-
* @param url - The full GitHub issue URL.
|
|
741
|
+
* Undismiss an issue or PR by URL.
|
|
742
|
+
* @param url - The full GitHub issue or PR URL.
|
|
743
743
|
* @returns true if found and removed, false if not dismissed.
|
|
744
744
|
*/
|
|
745
745
|
undismissIssue(url) {
|
|
@@ -750,8 +750,8 @@ export class StateManager {
|
|
|
750
750
|
return true;
|
|
751
751
|
}
|
|
752
752
|
/**
|
|
753
|
-
* Get the timestamp when an issue was dismissed.
|
|
754
|
-
* @param url - The full GitHub issue URL.
|
|
753
|
+
* Get the timestamp when an issue or PR was dismissed.
|
|
754
|
+
* @param url - The full GitHub issue or PR URL.
|
|
755
755
|
* @returns The ISO dismiss timestamp, or undefined if not dismissed.
|
|
756
756
|
*/
|
|
757
757
|
getIssueDismissedAt(url) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test factories for oss-autopilot.
|
|
3
|
+
*
|
|
4
|
+
* Centralises mock object construction so that when types gain new required
|
|
5
|
+
* fields we only update one place. Every factory accepts a `Partial<T>`
|
|
6
|
+
* override bag — callers only specify the fields relevant to their test.
|
|
7
|
+
*/
|
|
8
|
+
import type { FetchedPR, DailyDigest, ShelvedPRRef, AgentState } from './types.js';
|
|
9
|
+
import type { CapacityAssessment } from '../formatters/json.js';
|
|
10
|
+
export declare function makeFetchedPR(overrides?: Partial<FetchedPR>): FetchedPR;
|
|
11
|
+
export declare function makeDailyDigest(overrides?: Partial<DailyDigest>): DailyDigest;
|
|
12
|
+
export declare function makeShelvedPRRef(overrides?: Partial<ShelvedPRRef>): ShelvedPRRef;
|
|
13
|
+
export declare function makeCapacityAssessment(overrides?: Partial<CapacityAssessment>): CapacityAssessment;
|
|
14
|
+
export declare function makeAgentState(overrides?: Partial<AgentState>): AgentState;
|